CI / build-and-test (push) Canceled after 0s
Ein Zwischenstand ist ein Durchgang aus Stichtag, Anlass und Schülerauswahl (ganze Gruppe, Teilgruppe oder Einzelfall, mit Schnellfilter für gefährdete Schüler). Der Zeugnisnoten-Assistent geht die gewählten Schüler im Zwischenstand-Modus durch und notiert vorläufige schriftliche, mündliche und Gesamtnote samt Notiz. Gespeichert wird das als eigenes Modell InterimGrade, getrennt von der Zeugnisnote. Die Berechnung endet am Stichtag und rechnet den laufenden Mitarbeits-Abschnitt mit ein. Zwischenstände erscheinen im Assistenten als Liste und Marker im Verlauf sowie als graue Spalte in der Notenübersicht (nicht in "Gesamt", nicht im PDF). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
735 lines
36 KiB
C#
735 lines
36 KiB
C#
using LehrerApp.Core.Interfaces;
|
|
using LehrerApp.Core.Models;
|
|
using LehrerApp.Core.Services;
|
|
using LehrerApp.Desktop.Services;
|
|
using LehrerApp.Desktop.Services.Mcp;
|
|
using LehrerApp.Desktop.ViewModels.Settings;
|
|
using LehrerApp.Sync;
|
|
using LehrerApp.Sync.Crypto;
|
|
|
|
namespace LehrerApp.Desktop.Tests;
|
|
|
|
// Einfache In-Memory-Fakes der Repository-Schnittstellen, damit ViewModel-Tests ohne echte
|
|
// LiteDB-Anbindung laufen. Bewusst schlank gehalten: nur was die getesteten ViewModels brauchen.
|
|
|
|
public static class TestSupport
|
|
{
|
|
/// Für Tests, die eine echte AiSettingsService-Instanz brauchen (dateibasiert wie
|
|
/// PeriodScheduleService & Co.) — eigenes Temp-Verzeichnis je Aufruf, damit Tests sich nicht
|
|
/// gegenseitig über dieselbe ai-settings.json/ai-token.key stören.
|
|
public static AiSettingsService BuildAiSettingsService()
|
|
{
|
|
var tempPath = Path.Combine(Path.GetTempPath(), $"lehrerapp-aisettings-tests-{Guid.NewGuid():N}");
|
|
Directory.CreateDirectory(tempPath);
|
|
return new AiSettingsService(tempPath);
|
|
}
|
|
|
|
/// Für Tests, die nur einen validen AiPlanningService zum Durchreichen brauchen (z.B. weil
|
|
/// SettingsViewModel/PlanningTabViewModel ihn im Konstruktor verlangen), nicht seine eigentliche
|
|
/// Funktionalität testen — leere Fakes genügen, es wird kein echter HTTP-Aufruf ausgelöst,
|
|
/// solange AiSettingsService.IsLoggedIn false ist.
|
|
public static AiPlanningService BuildAiPlanningService() => new(
|
|
new HttpClient(), new FakeLessons(), new FakeGroups([]), new FakeSubjects([]),
|
|
new FakeCompetencyDomains(), new FakeCompetencyCatalogVersions(), new FakeAlternativeLessonPaths([]),
|
|
BuildAppLogger());
|
|
|
|
/// Analog zu <see cref="BuildAiSettingsService"/>, eigenes Temp-Verzeichnis je Aufruf.
|
|
public static McpSettingsService BuildMcpSettingsService()
|
|
{
|
|
var tempPath = Path.Combine(Path.GetTempPath(), $"lehrerapp-mcpsettings-tests-{Guid.NewGuid():N}");
|
|
Directory.CreateDirectory(tempPath);
|
|
return new McpSettingsService(tempPath);
|
|
}
|
|
|
|
/// Kein Konstruktorparameter - Pfade werden intern über Environment.SpecialFolder aufgelöst.
|
|
public static McpClientRegistrationService BuildMcpClientRegistrationService() => new();
|
|
|
|
/// Analog zu <see cref="BuildAiSettingsService"/>, eigenes Temp-Verzeichnis je Aufruf.
|
|
public static WebUntisSettingsService BuildWebUntisSettingsService()
|
|
{
|
|
var tempPath = Path.Combine(Path.GetTempPath(), $"lehrerapp-webuntissettings-tests-{Guid.NewGuid():N}");
|
|
Directory.CreateDirectory(tempPath);
|
|
return new WebUntisSettingsService(tempPath);
|
|
}
|
|
|
|
/// Nicht konfiguriert (kein API-Login hinterlegt) - genügt für Tests, die nur den Konstruktor
|
|
/// bedienen müssen und keinen echten WebUntis-Zugriff auslösen.
|
|
public static WebUntisIntegrationService BuildWebUntisIntegrationService() =>
|
|
new(new HttpClient(), BuildWebUntisSettingsService());
|
|
|
|
public static UntisHubService BuildUntisHubService(List<LearningGroup>? groups = null) =>
|
|
new(new FakeGroups(groups ?? []), new FakeUntisHubJobStates(), new SchoolYearService());
|
|
|
|
/// Analog zu den übrigen dateibasierten Feed-Einstellungen: eigenes Temp-Verzeichnis.
|
|
public static AnnualPlanSettingsService BuildAnnualPlanSettingsService()
|
|
{
|
|
var tempPath = Path.Combine(Path.GetTempPath(), $"lehrerapp-annualplansettings-tests-{Guid.NewGuid():N}");
|
|
Directory.CreateDirectory(tempPath);
|
|
return new AnnualPlanSettingsService(tempPath);
|
|
}
|
|
|
|
/// Analog zu <see cref="BuildAiSettingsService"/>, eigenes Temp-Verzeichnis je Aufruf.
|
|
public static SyncSettingsService BuildSyncSettingsService()
|
|
{
|
|
var tempPath = Path.Combine(Path.GetTempPath(), $"lehrerapp-syncsettings-tests-{Guid.NewGuid():N}");
|
|
Directory.CreateDirectory(tempPath);
|
|
return new SyncSettingsService(tempPath);
|
|
}
|
|
|
|
/// Kein echter HTTP-Aufruf, solange SyncSettingsService.IsLoggedIn false ist (siehe
|
|
/// BuildAiPlanningService).
|
|
public static SyncAuthService BuildSyncAuthService() => new(new HttpClient());
|
|
|
|
/// Eigenes Temp-Verzeichnis je Aufruf (echte, dateibasierte LiteDB wie bei EventQueue üblich).
|
|
public static EventQueue BuildEventQueue()
|
|
{
|
|
var tempPath = Path.Combine(Path.GetTempPath(), $"lehrerapp-eventqueue-tests-{Guid.NewGuid():N}");
|
|
Directory.CreateDirectory(tempPath);
|
|
return new EventQueue(Path.Combine(tempPath, "queue.db"));
|
|
}
|
|
|
|
/// Analog zu <see cref="BuildAiSettingsService"/>, eigenes Temp-Verzeichnis je Aufruf.
|
|
public static PeriodScheduleService BuildPeriodScheduleService()
|
|
{
|
|
var tempPath = Path.Combine(Path.GetTempPath(), $"lehrerapp-periodschedule-tests-{Guid.NewGuid():N}");
|
|
Directory.CreateDirectory(tempPath);
|
|
return new PeriodScheduleService(tempPath);
|
|
}
|
|
|
|
/// Analog zu <see cref="BuildAiSettingsService"/>, eigenes Temp-Verzeichnis je Aufruf.
|
|
public static AppLogger BuildAppLogger()
|
|
{
|
|
var tempPath = Path.Combine(Path.GetTempPath(), $"lehrerapp-applogger-tests-{Guid.NewGuid():N}");
|
|
Directory.CreateDirectory(tempPath);
|
|
return new AppLogger(tempPath);
|
|
}
|
|
|
|
/// Für Tests, die nicht speziell den "Schlüssel neu erzeugt"-Warnzustand prüfen.
|
|
public static SyncKeyStatus BuildSyncKeyStatus(bool keyWasRegenerated = false) => new(keyWasRegenerated);
|
|
|
|
/// Neue, leere Fakes je Aufruf - für Tests, die nicht speziell TrashViewModel-Verhalten prüfen.
|
|
public static TrashViewModel BuildTrashViewModel() => new(
|
|
new FakeTrash(), new FakeGrades(), new FakeWorkTasks(), new FakeTimeEntries(),
|
|
new FakeSeatingPlans(), new FakeGradingKeyTemplates());
|
|
|
|
/// Analog zu <see cref="BuildAiSettingsService"/>, eigenes Temp-Verzeichnis je Aufruf.
|
|
public static AppearanceSettingsService BuildAppearanceSettingsService()
|
|
{
|
|
var tempPath = Path.Combine(Path.GetTempPath(), $"lehrerapp-appearance-tests-{Guid.NewGuid():N}");
|
|
Directory.CreateDirectory(tempPath);
|
|
return new AppearanceSettingsService(tempPath);
|
|
}
|
|
|
|
/// Eigenes Temp-Verzeichnis je Aufruf, damit Tests sich nicht gegenseitig über dieselbe
|
|
/// sync.key stören.
|
|
public static SyncKeyRecoveryService BuildSyncKeyRecoveryService()
|
|
{
|
|
var tempPath = Path.Combine(Path.GetTempPath(), $"lehrerapp-synckeyrecovery-tests-{Guid.NewGuid():N}");
|
|
Directory.CreateDirectory(tempPath);
|
|
return new SyncKeyRecoveryService(SyncCrypto.GenerateKey(), Path.Combine(tempPath, "sync.key"));
|
|
}
|
|
}
|
|
|
|
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) =>
|
|
includeInactive ? all.ToList() : all.Where(s => s.IsActive).ToList();
|
|
public List<Student> GetByGroup(Guid groupId) => all;
|
|
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
|
|
{
|
|
public List<GroupMembership> GetByStudent(Guid studentId) => all.Where(m => m.StudentId == studentId).ToList();
|
|
public List<GroupMembership> GetByGroup(Guid groupId) => all.Where(m => m.GroupId == groupId).ToList();
|
|
public GroupMembership? GetByStudentAndGroup(Guid studentId, Guid groupId) =>
|
|
all.FirstOrDefault(m => m.StudentId == studentId && m.GroupId == groupId);
|
|
public void Save(GroupMembership membership)
|
|
{
|
|
all.RemoveAll(m => m.Id == membership.Id);
|
|
all.Add(membership);
|
|
}
|
|
public void Delete(Guid id) => all.RemoveAll(m => m.Id == id);
|
|
}
|
|
|
|
public class FakeSeatingPlans(List<SeatingPlan>? initial = null) : ISeatingPlanRepository
|
|
{
|
|
private readonly List<SeatingPlan> _all = initial ?? [];
|
|
public SeatingPlan? GetById(Guid id) => _all.FirstOrDefault(p => p.Id == id);
|
|
public List<SeatingPlan> GetByGroup(Guid groupId) =>
|
|
_all.Where(p => p.GroupId == groupId).OrderBy(p => p.Name).ToList();
|
|
public void Save(SeatingPlan plan)
|
|
{
|
|
_all.RemoveAll(p => p.Id == plan.Id);
|
|
_all.Add(plan);
|
|
}
|
|
public void Delete(Guid id) => _all.RemoveAll(p => p.Id == id);
|
|
// Trash-Mechanismus (14.3) lebt real in LiteDbContext.MoveToTrash/RestoreFromTrash - Fakes
|
|
// bilden ihn bewusst nicht nach, ViewModel-Tests brauchen das nicht.
|
|
public void Restore(Guid trashId) { }
|
|
}
|
|
|
|
public class FakeStudentGroupPlans : IStudentGroupPlanRepository
|
|
{
|
|
private readonly List<StudentGroupPlan> _plans = [];
|
|
public StudentGroupPlan? GetById(Guid id) => _plans.FirstOrDefault(p => p.Id == id);
|
|
public List<StudentGroupPlan> GetByGroup(Guid groupId) =>
|
|
_plans.Where(p => p.GroupId == groupId).ToList();
|
|
public List<StudentGroupPlan> GetByStudent(Guid studentId) =>
|
|
_plans.Where(p => p.Groups.Any(g => g.StudentIds.Contains(studentId))).ToList();
|
|
public void Save(StudentGroupPlan plan)
|
|
{
|
|
_plans.RemoveAll(p => p.Id == plan.Id);
|
|
_plans.Add(plan);
|
|
}
|
|
public void Delete(Guid id) => _plans.RemoveAll(p => p.Id == id);
|
|
}
|
|
|
|
public class FakeTrash(List<TrashedItem>? initial = null) : ITrashRepository
|
|
{
|
|
private readonly List<TrashedItem> _all = initial ?? [];
|
|
public void Add(TrashedItem item) => _all.Add(item);
|
|
public List<TrashedItem> GetAll() => _all.OrderByDescending(t => t.DeletedAt).ToList();
|
|
public void PurgeOlderThan(DateTime cutoffUtc) => _all.RemoveAll(t => t.DeletedAt < cutoffUtc);
|
|
}
|
|
|
|
public class FakeSessions(List<ParticipationSession> all) : IParticipationSessionRepository
|
|
{
|
|
public List<ParticipationSession> GetByGroup(Guid groupId) => all.Where(s => s.GroupId == groupId).ToList();
|
|
public ParticipationSession? GetById(Guid id) => all.FirstOrDefault(s => s.Id == id);
|
|
public List<ParticipationSession> GetAll() => all.ToList();
|
|
public void Save(ParticipationSession session)
|
|
{
|
|
all.RemoveAll(s => s.Id == session.Id);
|
|
all.Add(session);
|
|
}
|
|
public void Delete(Guid id) => all.RemoveAll(s => s.Id == id);
|
|
}
|
|
|
|
public class FakeEntries : IParticipationRepository
|
|
{
|
|
private readonly List<ParticipationEntry> _all = [];
|
|
public void Add(ParticipationEntry e) => _all.Add(e);
|
|
public List<ParticipationEntry> GetBySession(Guid sessionId) => _all.Where(e => e.SessionId == sessionId).ToList();
|
|
public List<ParticipationEntry> GetByStudent(Guid studentId) => _all.Where(e => e.StudentId == studentId).ToList();
|
|
public ParticipationEntry? GetBySessionAndStudent(Guid sessionId, Guid studentId) =>
|
|
_all.FirstOrDefault(e => e.SessionId == sessionId && e.StudentId == studentId);
|
|
public void Save(ParticipationEntry entry)
|
|
{
|
|
_all.RemoveAll(e => e.Id == entry.Id);
|
|
_all.Add(entry);
|
|
}
|
|
public void SaveMany(List<ParticipationEntry> entries) { foreach (var e in entries) Save(e); }
|
|
public void DeleteBySession(Guid sessionId) => _all.RemoveAll(e => e.SessionId == sessionId);
|
|
}
|
|
|
|
public class FakeAspects : IParticipationAspectRepository
|
|
{
|
|
private readonly List<ParticipationAspect> _all = [];
|
|
public List<ParticipationAspect> GetDefaults() =>
|
|
_all.Where(a => a.GroupId == null && a.IsActive).OrderBy(a => a.SortOrder).ToList();
|
|
public List<ParticipationAspect> GetByGroup(Guid groupId) =>
|
|
_all.Where(a => a.GroupId == groupId && a.IsActive).OrderBy(a => a.SortOrder).ToList();
|
|
public List<ParticipationAspect> GetAllByGroup(Guid groupId) =>
|
|
_all.Where(a => a.GroupId == groupId).OrderBy(a => a.SortOrder).ToList();
|
|
public void Save(ParticipationAspect a)
|
|
{
|
|
a.Key = a.Key.Trim(); a.Label = a.Label.Trim();
|
|
if (a.Key.Length == 0) throw new ArgumentException("Der Schlüssel darf nicht leer sein.");
|
|
if (a.Label.Length == 0) throw new ArgumentException("Die Bezeichnung darf nicht leer sein.");
|
|
var duplicate = _all.Where(x => x.GroupId == null || x.GroupId == a.GroupId)
|
|
.FirstOrDefault(x => x.Id != a.Id && string.Equals(x.Key, a.Key, StringComparison.OrdinalIgnoreCase));
|
|
if (duplicate is not null) throw new InvalidOperationException("Ein Aspekt mit diesem Schlüssel existiert für diese Gruppe bereits.");
|
|
_all.RemoveAll(x => x.Id == a.Id);
|
|
_all.Add(a);
|
|
}
|
|
public void Delete(Guid id) => _all.RemoveAll(a => a.Id == id);
|
|
}
|
|
|
|
public class FakeSections(List<ParticipationSection>? initial = null) : IParticipationSectionRepository
|
|
{
|
|
private readonly List<ParticipationSection> _all = initial ?? [];
|
|
public List<ParticipationSection> GetByGroup(Guid groupId) => _all.Where(s => s.GroupId == groupId).ToList();
|
|
public void Save(ParticipationSection section) { _all.RemoveAll(s => s.Id == section.Id); _all.Add(section); }
|
|
public void Delete(Guid id) => _all.RemoveAll(s => s.Id == id);
|
|
}
|
|
|
|
public class FakeGrades : IGradeRepository
|
|
{
|
|
private readonly List<Grade> _all = [];
|
|
public void Add(Grade g) => _all.Add(g);
|
|
public List<Grade> GetByStudentAndGroup(Guid studentId, Guid groupId) =>
|
|
_all.Where(g => g.StudentId == studentId && g.GroupId == groupId).ToList();
|
|
public List<Grade> GetByGroup(Guid groupId) => _all.Where(g => g.GroupId == groupId).ToList();
|
|
public void Save(Grade grade)
|
|
{
|
|
_all.RemoveAll(g => g.Id == grade.Id);
|
|
_all.Add(grade);
|
|
}
|
|
public void Delete(Guid id) => _all.RemoveAll(g => g.Id == id);
|
|
public void Restore(Guid trashId) { } // siehe FakeSeatingPlans.Restore
|
|
}
|
|
|
|
public class FakeExams(List<Exam> all) : IExamRepository
|
|
{
|
|
public Exam? GetById(Guid id) => all.FirstOrDefault(e => e.Id == id);
|
|
public List<Exam> GetAll() => all;
|
|
public List<Exam> GetByGroup(Guid groupId) => all.Where(e => e.GroupId == groupId).ToList();
|
|
public void Save(Exam exam) { }
|
|
public void Delete(Guid id) { }
|
|
}
|
|
|
|
public class FakeResults : IExamResultRepository
|
|
{
|
|
private readonly List<ExamResult> _all = [];
|
|
// Upsert-Semantik nach ExamId+StudentId, wie der reale unique Index es erzwingt.
|
|
public void Add(ExamResult r)
|
|
{
|
|
_all.RemoveAll(x => x.ExamId == r.ExamId && x.StudentId == r.StudentId);
|
|
_all.Add(r);
|
|
}
|
|
public List<ExamResult> GetByExam(Guid examId) => _all.Where(r => r.ExamId == examId).ToList();
|
|
public List<ExamResult> GetByStudent(Guid studentId) => _all.Where(r => r.StudentId == studentId).ToList();
|
|
public ExamResult? GetByExamAndStudent(Guid examId, Guid studentId) =>
|
|
_all.FirstOrDefault(r => r.ExamId == examId && r.StudentId == studentId);
|
|
public void Save(ExamResult result) { }
|
|
public void SaveMany(List<ExamResult> results) { }
|
|
}
|
|
|
|
public class FakeGradingKeyTemplates : IGradingKeyTemplateRepository
|
|
{
|
|
private readonly List<GradingKeyTemplate> _all = [];
|
|
public List<GradingKeyTemplate> GetAll() => _all;
|
|
public List<GradingKeyTemplate> GetByGradingSystem(GradingSystem system) =>
|
|
_all.Where(t => t.GradingSystem == system).ToList();
|
|
public GradingKeyTemplate? GetById(Guid id) => _all.FirstOrDefault(t => t.Id == id);
|
|
public void Save(GradingKeyTemplate template) { _all.RemoveAll(t => t.Id == template.Id); _all.Add(template); }
|
|
public void Delete(Guid id) => _all.RemoveAll(t => t.Id == id);
|
|
public void Restore(Guid trashId) { } // siehe FakeSeatingPlans.Restore
|
|
}
|
|
|
|
public class FakeSchemes : IGradingSchemeRepository
|
|
{
|
|
private readonly Dictionary<Guid, GradingScheme> _byGroup = [];
|
|
private readonly Dictionary<GroupType, GradingScheme> _byType = [];
|
|
public void SetForGroup(Guid groupId, GradingScheme s) => _byGroup[groupId] = s;
|
|
public void SetDefault(GroupType type, GradingScheme s) => _byType[type] = s;
|
|
public GradingScheme? GetByGroup(Guid groupId) => _byGroup.GetValueOrDefault(groupId);
|
|
public GradingScheme? GetDefaultForType(GroupType type) => _byType.GetValueOrDefault(type);
|
|
public void Save(GradingScheme scheme) { }
|
|
public void Delete(Guid id) { }
|
|
}
|
|
|
|
public class FakeGroups(List<LearningGroup> all) : IGroupRepository
|
|
{
|
|
public LearningGroup? GetById(Guid id) => all.FirstOrDefault(g => g.Id == id);
|
|
public List<LearningGroup> GetAll(bool includeInactive = false) => all;
|
|
public List<LearningGroup> GetBySchoolYear(string schoolYear, bool includeInactive = false) => all;
|
|
public void Save(LearningGroup group) { }
|
|
public void Delete(Guid id) { }
|
|
}
|
|
|
|
public class FakeDocumentation : IDocumentationRepository
|
|
{
|
|
private readonly List<Documentation> _all = [];
|
|
public void Add(Documentation d) => _all.Add(d);
|
|
public List<Documentation> GetByStudent(Guid studentId) =>
|
|
_all.Where(d => d.Concerns(studentId) && !d.IsDeleted).ToList();
|
|
public List<Documentation> GetByStudentAndType(Guid studentId, DocumentationType type) =>
|
|
_all.Where(d => d.Concerns(studentId) && d.Type == type && !d.IsDeleted).ToList();
|
|
public List<Documentation> GetAll() => _all.Where(d => !d.IsDeleted).ToList();
|
|
public void Save(Documentation doc) { _all.RemoveAll(d => d.Id == doc.Id); _all.Add(doc); }
|
|
public void Delete(Guid id) { var d = _all.FirstOrDefault(x => x.Id == id); if (d is not null) d.IsDeleted = true; }
|
|
public void HardDelete(Guid id) => _all.RemoveAll(d => d.Id == id);
|
|
}
|
|
|
|
public class FakeVorgangRepository : IVorgangRepository
|
|
{
|
|
private readonly List<Vorgang> _all = [];
|
|
public void Add(Vorgang v) => _all.Add(v);
|
|
public List<Vorgang> GetAll() => _all.Where(v => !v.IsDeleted).ToList();
|
|
public List<Vorgang> GetByStudent(Guid studentId) =>
|
|
_all.Where(v => !v.IsDeleted && v.StudentIds.Contains(studentId)).ToList();
|
|
public Vorgang? GetById(Guid id) => _all.FirstOrDefault(v => v.Id == id && !v.IsDeleted);
|
|
public void Save(Vorgang vorgang) { _all.RemoveAll(v => v.Id == vorgang.Id); _all.Add(vorgang); }
|
|
public void Delete(Guid id) { var v = _all.FirstOrDefault(x => x.Id == id); if (v is not null) v.IsDeleted = true; }
|
|
}
|
|
|
|
public class FakeDashboardWarningSnoozeRepository : IDashboardWarningSnoozeRepository
|
|
{
|
|
private readonly List<DashboardWarningSnooze> _all = [];
|
|
public DashboardWarningSnooze? Get(Guid studentId, string warningTag) =>
|
|
_all.FirstOrDefault(s => s.StudentId == studentId && s.WarningTag == warningTag);
|
|
public List<DashboardWarningSnooze> GetAll() => _all.ToList();
|
|
public void Save(DashboardWarningSnooze snooze)
|
|
{
|
|
snooze.Id = DashboardWarningSnooze.IdFor(snooze.StudentId, snooze.WarningTag);
|
|
_all.RemoveAll(s => s.Id == snooze.Id);
|
|
_all.Add(snooze);
|
|
}
|
|
}
|
|
|
|
public class FakeAttachmentStorage : IAttachmentStorage
|
|
{
|
|
private readonly Dictionary<string, byte[]> _blobs = [];
|
|
public string Upload(string fileName, Stream content)
|
|
{
|
|
using var ms = new MemoryStream();
|
|
content.CopyTo(ms);
|
|
var id = Guid.NewGuid().ToString("N");
|
|
_blobs[id] = ms.ToArray();
|
|
return id;
|
|
}
|
|
public Stream? OpenRead(string storageId) => _blobs.TryGetValue(storageId, out var b) ? new MemoryStream(b) : null;
|
|
public void Delete(string storageId) => _blobs.Remove(storageId);
|
|
}
|
|
|
|
public class FakeUnits : IUnitRepository
|
|
{
|
|
private readonly List<Unit> _all = [];
|
|
public void Add(Unit u) => _all.Add(u);
|
|
public Unit? GetById(Guid id) => _all.FirstOrDefault(u => u.Id == id);
|
|
public List<Unit> GetByGroup(Guid groupId) =>
|
|
_all.Where(u => u.GroupId == groupId).OrderBy(u => u.StartDate).ToList();
|
|
public void Save(Unit unit) { _all.RemoveAll(u => u.Id == unit.Id); _all.Add(unit); }
|
|
public void Delete(Guid id) => _all.RemoveAll(u => u.Id == id);
|
|
}
|
|
|
|
public class FakeLessons : ILessonRepository
|
|
{
|
|
private readonly List<Lesson> _all = [];
|
|
public void Add(Lesson l) => _all.Add(l);
|
|
public Lesson? GetById(Guid id) => _all.FirstOrDefault(l => l.Id == id);
|
|
public List<Lesson> GetByUnit(Guid unitId) =>
|
|
_all.Where(l => l.UnitId == unitId).OrderBy(l => l.Date).ThenBy(l => l.LessonNumber).ToList();
|
|
public List<Lesson> GetByGroupAndDate(Guid groupId, DateOnly date) =>
|
|
_all.Where(l => l.GroupId == groupId && l.Date == date).ToList();
|
|
public List<Lesson> GetByGroupAndRange(Guid groupId, DateOnly from, DateOnly to) =>
|
|
_all.Where(l => l.GroupId == groupId && l.Date >= from && l.Date <= to).OrderBy(l => l.Date).ToList();
|
|
public void Save(Lesson lesson) { _all.RemoveAll(l => l.Id == lesson.Id); _all.Add(lesson); }
|
|
public void Delete(Guid id) => _all.RemoveAll(l => l.Id == id);
|
|
}
|
|
|
|
public class FakeSubjects(List<Subject> all) : ISubjectRepository
|
|
{
|
|
public List<Subject> GetAll() => all;
|
|
public Subject? GetById(Guid id) => all.FirstOrDefault(s => s.Id == id);
|
|
public Subject? GetByName(string name) => all.FirstOrDefault(s =>
|
|
string.Equals(s.Name.Trim(), name.Trim(), StringComparison.OrdinalIgnoreCase));
|
|
public void Save(Subject subject)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(subject.Name)) throw new ArgumentException("Der Fachname darf nicht leer sein.");
|
|
var duplicate = GetByName(subject.Name);
|
|
if (duplicate is not null && duplicate.Id != subject.Id)
|
|
throw new InvalidOperationException("Ein Fach mit diesem Namen existiert bereits.");
|
|
all.RemoveAll(s => s.Id == subject.Id);
|
|
all.Add(subject);
|
|
}
|
|
public void Delete(Guid id) => all.RemoveAll(s => s.Id == id);
|
|
}
|
|
|
|
public class FakeCompetencyDomains : ICompetencyDomainRepository
|
|
{
|
|
private readonly List<CompetencyDomain> _all = [];
|
|
public void Add(CompetencyDomain d) => _all.Add(d);
|
|
public List<CompetencyDomain> GetBySubjectAndGrade(Guid subjectId, int gradeLevel, Guid versionId) =>
|
|
_all.Where(d => d.SubjectId == subjectId && d.GradeLevel == gradeLevel && d.VersionId == versionId)
|
|
.OrderBy(d => d.SortOrder).ToList();
|
|
public List<CompetencyDomain> GetBySubjectAndVersion(Guid subjectId, Guid versionId) =>
|
|
_all.Where(d => d.SubjectId == subjectId && d.VersionId == versionId)
|
|
.OrderBy(d => d.GradeLevel).ThenBy(d => d.SortOrder).ToList();
|
|
public List<CompetencyDomain> GetAll() => _all.ToList();
|
|
public CompetencyDomain? GetById(Guid id) => _all.FirstOrDefault(d => d.Id == id);
|
|
public void Save(CompetencyDomain domain) { _all.RemoveAll(d => d.Id == domain.Id); _all.Add(domain); }
|
|
public void Delete(Guid id) => _all.RemoveAll(d => d.Id == id);
|
|
public void DeleteBySubjectGradeAndVersion(Guid subjectId, int gradeLevel, Guid versionId) =>
|
|
_all.RemoveAll(d => d.SubjectId == subjectId && d.GradeLevel == gradeLevel && d.VersionId == versionId);
|
|
public void ReplaceForSubjectGradeAndVersion(Guid subjectId, int gradeLevel, Guid versionId, List<CompetencyDomain> domains)
|
|
{
|
|
DeleteBySubjectGradeAndVersion(subjectId, gradeLevel, versionId);
|
|
foreach (var d in domains) { d.SubjectId = subjectId; d.GradeLevel = gradeLevel; d.VersionId = versionId; }
|
|
_all.AddRange(domains);
|
|
}
|
|
}
|
|
|
|
public class FakeCompetencyCatalogVersions : ICompetencyCatalogVersionRepository
|
|
{
|
|
private readonly List<CompetencyCatalogVersion> _all = [];
|
|
public void Add(CompetencyCatalogVersion v) => _all.Add(v);
|
|
public List<CompetencyCatalogVersion> GetBySubject(Guid subjectId) =>
|
|
_all.Where(v => v.SubjectId == subjectId).OrderBy(v => v.SortOrder).ToList();
|
|
public CompetencyCatalogVersion? GetById(Guid id) => _all.FirstOrDefault(v => v.Id == id);
|
|
public CompetencyCatalogVersion GetOrCreateDefault(Guid subjectId)
|
|
{
|
|
var existing = GetBySubject(subjectId).FirstOrDefault(v => v.IsDefault);
|
|
if (existing is not null) return existing;
|
|
var version = new CompetencyCatalogVersion { SubjectId = subjectId, Name = "Bestehender Kompetenzkatalog", IsDefault = true };
|
|
Save(version);
|
|
return version;
|
|
}
|
|
public void Save(CompetencyCatalogVersion version)
|
|
{
|
|
if (version.IsDefault)
|
|
foreach (var other in _all.Where(v => v.SubjectId == version.SubjectId && v.Id != version.Id)) other.IsDefault = false;
|
|
_all.RemoveAll(v => v.Id == version.Id);
|
|
_all.Add(version);
|
|
}
|
|
public void Delete(Guid id)
|
|
{
|
|
var version = _all.FirstOrDefault(v => v.Id == id);
|
|
if (version is null) return;
|
|
if (version.IsDefault)
|
|
throw new InvalidOperationException("Die Standardversion eines Fachs kann nicht gelöscht werden. Erst eine andere Version als Standard setzen.");
|
|
_all.RemoveAll(v => v.Id == id);
|
|
}
|
|
}
|
|
|
|
public class FakeShorthandCodes(List<ShorthandCode> all) : IShorthandCodeRepository
|
|
{
|
|
public List<ShorthandCode> GetAll() => all;
|
|
public void Save(ShorthandCode code) { }
|
|
public void Delete(Guid id) { }
|
|
}
|
|
|
|
public class FakeAlternativeLessonPaths(List<AlternativeLessonPath> all) : IAlternativeLessonPathRepository
|
|
{
|
|
public List<AlternativeLessonPath> GetAll() => all;
|
|
public AlternativeLessonPath? GetById(Guid id) => all.FirstOrDefault(p => p.Id == id);
|
|
public void Save(AlternativeLessonPath path) { all.RemoveAll(p => p.Id == path.Id); all.Add(path); }
|
|
public void Delete(Guid id) => all.RemoveAll(p => p.Id == id);
|
|
}
|
|
|
|
public class FakeTimetableSlots : ITimetableSlotRepository
|
|
{
|
|
private readonly List<TimetableSlot> _all = [];
|
|
public void Add(TimetableSlot s) => _all.Add(s);
|
|
public List<TimetableSlot> GetAll() => _all.ToList();
|
|
public List<TimetableSlot> GetByGroup(Guid groupId) => _all.Where(s => s.GroupId == groupId).ToList();
|
|
public void Save(TimetableSlot slot)
|
|
{
|
|
var occupied = _all.FirstOrDefault(s => s.Weekday == slot.Weekday && s.PeriodNumber == slot.PeriodNumber);
|
|
if (occupied is not null && occupied.Id != slot.Id)
|
|
throw new InvalidOperationException("Diese Stunde ist bereits belegt.");
|
|
_all.RemoveAll(s => s.Id == slot.Id);
|
|
_all.Add(slot);
|
|
}
|
|
public void Delete(Guid id) => _all.RemoveAll(s => s.Id == id);
|
|
}
|
|
|
|
public class FakeSchoolHolidays : ISchoolHolidayRepository
|
|
{
|
|
private readonly List<SchoolHoliday> _all = [];
|
|
public void Add(SchoolHoliday h) => _all.Add(h);
|
|
public List<SchoolHoliday> GetAll() => _all.ToList();
|
|
public void Save(SchoolHoliday holiday) { _all.RemoveAll(h => h.Id == holiday.Id); _all.Add(holiday); }
|
|
public void Delete(Guid id) => _all.RemoveAll(h => h.Id == id);
|
|
}
|
|
|
|
public class FakeSupervisionDuties : ISupervisionDutyRepository
|
|
{
|
|
private readonly List<SupervisionDuty> _all = [];
|
|
public void Add(SupervisionDuty d) => _all.Add(d);
|
|
public List<SupervisionDuty> GetAll() => _all.ToList();
|
|
public void Save(SupervisionDuty duty)
|
|
{
|
|
var occupied = _all.FirstOrDefault(d => d.Weekday == duty.Weekday && d.AfterPeriod == duty.AfterPeriod);
|
|
if (occupied is not null && occupied.Id != duty.Id)
|
|
throw new InvalidOperationException("Für diese Pause ist bereits eine Aufsicht eingetragen.");
|
|
_all.RemoveAll(d => d.Id == duty.Id);
|
|
_all.Add(duty);
|
|
}
|
|
public void Delete(Guid id) => _all.RemoveAll(d => d.Id == id);
|
|
}
|
|
|
|
public class FakeSubstitutionEntries : ISubstitutionEntryRepository
|
|
{
|
|
private readonly List<SubstitutionEntry> _all = [];
|
|
public void Add(SubstitutionEntry e) => _all.Add(e);
|
|
public List<SubstitutionEntry> GetAll() => _all.ToList();
|
|
public List<SubstitutionEntry> GetByDate(DateOnly date) => _all.Where(e => e.Date == date).ToList();
|
|
public SubstitutionEntry? GetByExternalId(string externalId) => _all.FirstOrDefault(e => e.ExternalId == externalId);
|
|
public void Save(SubstitutionEntry entry) { _all.RemoveAll(e => e.Id == entry.Id); _all.Add(entry); }
|
|
public void Delete(Guid id) => _all.RemoveAll(e => e.Id == id);
|
|
}
|
|
|
|
public class FakeUntisSnapshots : IUntisSnapshotRepository
|
|
{
|
|
private readonly List<UntisSnapshotEntry> _all = [];
|
|
public void Add(UntisSnapshotEntry e) => _all.Add(e);
|
|
public List<UntisSnapshotEntry> GetAll() => _all.ToList();
|
|
public void Save(UntisSnapshotEntry entry) { _all.RemoveAll(e => e.Id == entry.Id); _all.Add(entry); }
|
|
public void Delete(Guid id) => _all.RemoveAll(e => e.Id == id);
|
|
}
|
|
|
|
public class FakeUntisSlotMappings : IUntisSlotMappingRepository
|
|
{
|
|
private readonly List<UntisSlotMapping> _all = [];
|
|
public void Add(UntisSlotMapping m) => _all.Add(m);
|
|
public List<UntisSlotMapping> GetAll() => _all.ToList();
|
|
public void Save(UntisSlotMapping mapping) { _all.RemoveAll(m => m.Id == mapping.Id); _all.Add(mapping); }
|
|
public void Delete(Guid id) => _all.RemoveAll(m => m.Id == id);
|
|
}
|
|
|
|
public class FakeAnnualPlanEvents : IAnnualPlanEventRepository
|
|
{
|
|
private readonly List<AnnualPlanEvent> _all = [];
|
|
public void Add(AnnualPlanEvent e) => _all.Add(e);
|
|
public List<AnnualPlanEvent> GetAll() => _all.ToList();
|
|
public List<AnnualPlanEvent> GetByRange(DateOnly from, DateOnly to) =>
|
|
_all.Where(e => e.StartDate <= to && e.EndDate >= from).ToList();
|
|
public AnnualPlanEvent? GetByExternalId(string externalId) =>
|
|
_all.FirstOrDefault(e => e.ExternalId == externalId);
|
|
public void Save(AnnualPlanEvent entry) { _all.RemoveAll(e => e.Id == entry.Id); _all.Add(entry); }
|
|
public void Delete(Guid id) => _all.RemoveAll(e => e.Id == id);
|
|
}
|
|
|
|
public class FakeUntisAbsenceCache : IUntisAbsenceCacheRepository
|
|
{
|
|
private readonly List<UntisAbsenceCacheEntry> _all = [];
|
|
public List<UntisAbsenceCacheEntry> GetByClassAndRange(string className, int startDate, int endDate) =>
|
|
_all.Where(e => e.ClassName == className && e.Date >= startDate && e.Date <= endDate).ToList();
|
|
public void ReplaceRange(string className, int startDate, int endDate, IEnumerable<UntisAbsenceCacheEntry> entries)
|
|
{
|
|
_all.RemoveAll(e => e.ClassName == className && e.Date >= startDate && e.Date <= endDate);
|
|
_all.AddRange(entries);
|
|
}
|
|
public void InsertRange(IEnumerable<UntisAbsenceCacheEntry> entries) => _all.AddRange(entries);
|
|
}
|
|
|
|
public class FakeUntisClassRegisterCache : IUntisClassRegisterCacheRepository
|
|
{
|
|
private readonly List<UntisClassRegisterCacheEntry> _all = [];
|
|
public List<UntisClassRegisterCacheEntry> GetByClassAndRange(string className, int startDate, int endDate) =>
|
|
_all.Where(e => e.ClassName == className && e.Date >= startDate && e.Date <= endDate).ToList();
|
|
public void ReplaceRange(string className, int startDate, int endDate, IEnumerable<UntisClassRegisterCacheEntry> entries)
|
|
{
|
|
_all.RemoveAll(e => e.ClassName == className && e.Date >= startDate && e.Date <= endDate);
|
|
_all.AddRange(entries);
|
|
}
|
|
public void InsertRange(IEnumerable<UntisClassRegisterCacheEntry> entries) => _all.AddRange(entries);
|
|
}
|
|
|
|
public class FakeUntisStudentRosterCache : IUntisStudentRosterCacheRepository
|
|
{
|
|
private readonly List<UntisStudentRosterCacheEntry> _all = [];
|
|
public List<UntisStudentRosterCacheEntry> GetByClass(string className) =>
|
|
_all.Where(e => e.ClassName == className).ToList();
|
|
public void ReplaceAll(string className, IEnumerable<UntisStudentRosterCacheEntry> entries)
|
|
{
|
|
_all.RemoveAll(e => e.ClassName == className);
|
|
_all.AddRange(entries);
|
|
}
|
|
}
|
|
|
|
public class FakeUntisCacheFetchStates : IUntisCacheFetchStateRepository
|
|
{
|
|
private readonly List<UntisCacheFetchState> _all = [];
|
|
public UntisCacheFetchState? Get(string className, UntisCacheKind kind) =>
|
|
_all.FirstOrDefault(s => s.ClassName == className && s.Kind == kind);
|
|
public void Save(UntisCacheFetchState state)
|
|
{
|
|
_all.RemoveAll(s => s.ClassName == state.ClassName && s.Kind == state.Kind);
|
|
_all.Add(state);
|
|
}
|
|
}
|
|
|
|
public class FakeUntisHubJobStates : IUntisHubJobStateRepository
|
|
{
|
|
private readonly List<UntisHubJobState> _all = [];
|
|
public UntisHubJobState? Get(UntisHubJobKind kind, Guid? groupId) =>
|
|
_all.FirstOrDefault(s => s.Kind == kind && s.GroupId == groupId);
|
|
public List<UntisHubJobState> GetAll() => _all.ToList();
|
|
public void Save(UntisHubJobState state)
|
|
{
|
|
_all.RemoveAll(s => s.Kind == state.Kind && s.GroupId == state.GroupId);
|
|
_all.Add(state);
|
|
}
|
|
}
|
|
|
|
public class FakeWorkTasks : IWorkTaskRepository
|
|
{
|
|
private readonly List<WorkTask> _all = [];
|
|
public void Add(WorkTask t) => _all.Add(t);
|
|
public List<WorkTask> GetByStatus(WorkTaskStatus status) => _all.Where(t => t.Status == status).ToList();
|
|
public List<WorkTask> GetAll() => _all.ToList();
|
|
public List<WorkTask> GetByGroup(Guid groupId) => _all.Where(t => t.GroupId == groupId).ToList();
|
|
public void Save(WorkTask task) { _all.RemoveAll(t => t.Id == task.Id); _all.Add(task); }
|
|
public void Delete(Guid id) => _all.RemoveAll(t => t.Id == id);
|
|
public void Restore(Guid trashId) { } // siehe FakeSeatingPlans.Restore
|
|
}
|
|
|
|
public class FakeTimeEntries : ITimeEntryRepository
|
|
{
|
|
private readonly List<TimeEntry> _all = [];
|
|
public void Add(TimeEntry e) => _all.Add(e);
|
|
public List<TimeEntry> GetByDate(DateOnly date) => _all.Where(e => e.Date == date).ToList();
|
|
public List<TimeEntry> GetByDateRange(DateOnly from, DateOnly to) =>
|
|
_all.Where(e => e.Date >= from && e.Date <= to).ToList();
|
|
public List<TimeEntry> GetByTask(Guid taskId) => _all.Where(e => e.TaskId == taskId).ToList();
|
|
public void Save(TimeEntry entry) { _all.RemoveAll(e => e.Id == entry.Id); _all.Add(entry); }
|
|
public void Delete(Guid id) => _all.RemoveAll(e => e.Id == id);
|
|
public void Restore(Guid trashId) { } // siehe FakeSeatingPlans.Restore
|
|
}
|
|
|
|
public class FakeInterimGrades : IInterimGradeRepository
|
|
{
|
|
private readonly List<InterimGrade> _all = [];
|
|
public List<InterimGrade> All => _all;
|
|
public List<InterimGrade> GetByGroup(Guid groupId) => _all.Where(g => g.GroupId == groupId).ToList();
|
|
public List<InterimGrade> GetByStudentAndGroup(Guid studentId, Guid groupId) =>
|
|
_all.Where(g => g.StudentId == studentId && g.GroupId == groupId).ToList();
|
|
public void Save(InterimGrade grade) { _all.RemoveAll(g => g.Id == grade.Id); _all.Add(grade); }
|
|
public void Delete(Guid id) => _all.RemoveAll(g => g.Id == id);
|
|
}
|
|
|
|
public class FakeReportGrades : IReportGradeRepository
|
|
{
|
|
private readonly List<ReportGrade> _all = [];
|
|
public int SavedCount { get; private set; }
|
|
public List<ReportGrade> GetByGroup(Guid groupId) => _all.Where(r => r.GroupId == groupId).ToList();
|
|
public ReportGrade? GetByStudentGroupPeriod(Guid studentId, Guid groupId, string period) =>
|
|
_all.FirstOrDefault(r => r.StudentId == studentId && r.GroupId == groupId && r.Period == period);
|
|
public void Save(ReportGrade grade)
|
|
{
|
|
_all.RemoveAll(r => r.Id == grade.Id);
|
|
_all.Add(grade);
|
|
SavedCount++;
|
|
}
|
|
public void Delete(Guid id) => _all.RemoveAll(r => r.Id == id);
|
|
}
|
|
|
|
/// <summary>Fake für MCP-Write-Tool-Tests (Phase 2): antwortet ohne echtes UI, konfigurierbar über
|
|
/// <see cref="Response"/>, merkt sich Titel/Nachricht des letzten Aufrufs zur Prüfung, dass der
|
|
/// Bestätigungstext tatsächlich menschenlesbar ist (kein rohes JSON/GUID-Dump).</summary>
|
|
public class FakeMcpConfirmation : IMcpConfirmationService
|
|
{
|
|
public bool Response { get; set; } = true;
|
|
public string? LastTitle { get; private set; }
|
|
public string? LastMessage { get; private set; }
|
|
public int CallCount { get; private set; }
|
|
|
|
public string? LastOperationKey { get; private set; }
|
|
public bool? LastAllowSessionTrust { get; private set; }
|
|
|
|
public Task<bool> ConfirmAsync(string title, string message, CancellationToken ct,
|
|
[System.Runtime.CompilerServices.CallerMemberName] string operationKey = "", bool allowSessionTrust = true)
|
|
{
|
|
CallCount++;
|
|
LastTitle = title;
|
|
LastMessage = message;
|
|
LastOperationKey = operationKey;
|
|
LastAllowSessionTrust = allowSessionTrust;
|
|
return Task.FromResult(Response);
|
|
}
|
|
}
|