using LehrerApp.Core.Models; using LiteDB; using Xunit; namespace LehrerApp.Data.Tests; public sealed class LiteDbContextTests { [Fact] public void MigratesLegacyEnrollmentAndSubjectTextWithoutLosingData() { using var temp = new TempDatabase(); var groupId = Guid.NewGuid(); var studentId = Guid.NewGuid(); var membershipId = Guid.NewGuid(); var examId = Guid.NewGuid(); var gradeId = Guid.NewGuid(); var entryId = Guid.NewGuid(); var addedOn = new DateOnly(2026, 6, 23); using (var legacy = new LiteDatabase(temp.Path)) { legacy.GetCollection("subjects").Insert(new Subject { Name = "mathematik" }); legacy.GetCollection("groups").Insert(new BsonDocument { ["_id"] = groupId, [nameof(LearningGroup.Name)] = "Mathe G", ["Subject"] = "Mathematik", [nameof(LearningGroup.SchoolYear)] = "2025/26", }); legacy.GetCollection("enrollments").Insert(new BsonDocument { ["_id"] = membershipId, [nameof(GroupMembership.StudentId)] = studentId, [nameof(GroupMembership.GroupId)] = groupId, ["SchoolYear"] = "2025/26", ["EnrolledAt"] = BsonMapper.Global.Serialize(addedOn), }); legacy.GetCollection("exams").Insert(new BsonDocument { ["_id"] = examId, [nameof(Exam.GroupId)] = groupId, ["Subject"] = "Mathematik", }); legacy.GetCollection("grades").Insert(new BsonDocument { ["_id"] = gradeId, [nameof(Grade.StudentId)] = studentId, [nameof(Grade.GroupId)] = groupId, ["SchoolYear"] = "2025/26", }); legacy.GetCollection("participation").Insert(new BsonDocument { ["_id"] = entryId, [nameof(ParticipationEntry.SessionId)] = Guid.NewGuid(), [nameof(ParticipationEntry.StudentId)] = studentId, ["GroupId"] = groupId, ["Date"] = BsonMapper.Global.Serialize(addedOn), }); } using (var context = new LiteDbContext(temp.Path)) { var group = context.Groups.FindById(groupId); var membership = context.Memberships.FindById(membershipId); Assert.NotNull(group); Assert.NotNull(group.SubjectId); Assert.Equal("mathematik", context.Subjects.FindById(group.SubjectId.Value)?.Name); Assert.Equal(1, context.Subjects.Count()); Assert.NotNull(membership); Assert.Equal(addedOn, membership.AddedOn); } using var migrated = new LiteDatabase(temp.Path); Assert.DoesNotContain("enrollments", migrated.GetCollectionNames()); Assert.Contains("group_memberships", migrated.GetCollectionNames()); var rawGroup = migrated.GetCollection("groups").FindById(groupId); var rawMembership = migrated.GetCollection("group_memberships").FindById(membershipId); var rawExam = migrated.GetCollection("exams").FindById(examId); var rawGrade = migrated.GetCollection("grades").FindById(gradeId); var rawEntry = migrated.GetCollection("participation").FindById(entryId); Assert.False(rawGroup.ContainsKey("Subject")); Assert.False(rawMembership.ContainsKey("SchoolYear")); Assert.False(rawMembership.ContainsKey("EnrolledAt")); Assert.True(rawMembership.ContainsKey(nameof(GroupMembership.AddedOn))); Assert.False(rawExam.ContainsKey("Subject")); Assert.False(rawGrade.ContainsKey("SchoolYear")); Assert.False(rawEntry.ContainsKey("GroupId")); Assert.False(rawEntry.ContainsKey("Date")); } [Fact] public void RejectsASecondMembershipForTheSameStudentAndGroup() { using var temp = new TempDatabase(); using var context = new LiteDbContext(temp.Path); var studentId = Guid.NewGuid(); var groupId = Guid.NewGuid(); context.Memberships.Insert(new GroupMembership { StudentId = studentId, GroupId = groupId }); Assert.Throws(() => context.Memberships.Insert( new GroupMembership { StudentId = studentId, GroupId = groupId })); } [Fact] public void RejectsASecondExamResultForTheSameStudentAndExam() { using var temp = new TempDatabase(); using var context = new LiteDbContext(temp.Path); var studentId = Guid.NewGuid(); var examId = Guid.NewGuid(); context.ExamResults.Insert(new ExamResult { StudentId = studentId, ExamId = examId }); Assert.Throws(() => context.ExamResults.Insert( new ExamResult { StudentId = studentId, ExamId = examId })); } [Fact] public void NeueDatenbank_HatDieAktuelleSchemaVersionNachDemErstenOeffnen() { using var temp = new TempDatabase(); using var context = new LiteDbContext(temp.Path); Assert.Equal(4, context.SchemaVersion); } [Fact] public void SchemaVersion_WirdUeberEinenNeustartHinwegBeibehalten() { using var temp = new TempDatabase(); using (var first = new LiteDbContext(temp.Path)) { } using var second = new LiteDbContext(temp.Path); Assert.Equal(4, second.SchemaVersion); } [Fact] public void MigrateLessonPhases_FasstAltePhaseMethodenUndMaterialienVerlustfreiZusammen() { using var temp = new TempDatabase(); var lessonWithDataId = Guid.NewGuid(); var lessonEmptyId = Guid.NewGuid(); using (var legacy = new LiteDatabase(temp.Path)) { legacy.GetCollection("lessons").Insert(new BsonDocument { ["_id"] = lessonWithDataId, [nameof(Lesson.UnitId)] = Guid.NewGuid(), [nameof(Lesson.GroupId)] = Guid.NewGuid(), ["Phase"] = "Einstieg", ["Methods"] = new BsonArray { "Gespräch", "Tafelbild" }, ["Materials"] = new BsonArray { "Arbeitsblatt" }, }); // Alte Stunde ganz ohne Phase/Methoden/Materialien — soll leere Phases-Liste bekommen, // nicht eine leere synthetisierte Zeile. legacy.GetCollection("lessons").Insert(new BsonDocument { ["_id"] = lessonEmptyId, [nameof(Lesson.UnitId)] = Guid.NewGuid(), [nameof(Lesson.GroupId)] = Guid.NewGuid(), }); } using (var context = new LiteDbContext(temp.Path)) { var withData = context.Lessons.FindById(lessonWithDataId); Assert.NotNull(withData); Assert.Single(withData.Phases); Assert.Equal("Einstieg", withData.Phases[0].Name); Assert.Equal("Gespräch; Tafelbild", withData.Phases[0].Activity); Assert.Equal("Arbeitsblatt", withData.Phases[0].Material); Assert.Equal(0, withData.Phases[0].DurationMinutes); var empty = context.Lessons.FindById(lessonEmptyId); Assert.NotNull(empty); Assert.Empty(empty.Phases); } using var migrated = new LiteDatabase(temp.Path); var raw = migrated.GetCollection("lessons").FindById(lessonWithDataId); Assert.False(raw.ContainsKey("Phase")); Assert.False(raw.ContainsKey("Methods")); Assert.False(raw.ContainsKey("Materials")); } [Fact] public void MigrateLessonShorthand_FasstVonUndNachInEinFreitextfeldZusammen() { using var temp = new TempDatabase(); var lessonId = Guid.NewGuid(); using (var legacy = new LiteDatabase(temp.Path)) { legacy.GetCollection("lessons").Insert(new BsonDocument { ["_id"] = lessonId, [nameof(Lesson.UnitId)] = Guid.NewGuid(), [nameof(Lesson.GroupId)] = Guid.NewGuid(), [nameof(Lesson.Phases)] = new BsonArray { new BsonDocument { ["ShorthandFrom"] = "Tb", ["ShorthandTo"] = "SH" }, new BsonDocument { ["ShorthandFrom"] = "Plenum" }, // nur "Von" gesetzt, kein Pfeil new BsonDocument { }, // weder Von noch Nach }, }); } using (var context = new LiteDbContext(temp.Path)) { var lesson = context.Lessons.FindById(lessonId); Assert.NotNull(lesson); Assert.Equal(3, lesson.Phases.Count); Assert.Equal("Tb->SH", lesson.Phases[0].Shorthand); Assert.Equal("Plenum", lesson.Phases[1].Shorthand); Assert.Equal("", lesson.Phases[2].Shorthand); } using var migratedShorthand = new LiteDatabase(temp.Path); var rawLesson = migratedShorthand.GetCollection("lessons").FindById(lessonId); var rawPhases = rawLesson["Phases"].AsArray; Assert.False(rawPhases[0].AsDocument.ContainsKey("ShorthandFrom")); Assert.False(rawPhases[0].AsDocument.ContainsKey("ShorthandTo")); } [Fact] public void MigrateLessonAlternativePaths_ErzeugtKatalogEintraegeUndVerweistDarauf() { using var temp = new TempDatabase(); var lesson1Id = Guid.NewGuid(); var lesson2Id = Guid.NewGuid(); using (var legacy = new LiteDatabase(temp.Path)) { legacy.GetCollection("lessons").Insert(new BsonDocument { ["_id"] = lesson1Id, [nameof(Lesson.UnitId)] = Guid.NewGuid(), [nameof(Lesson.GroupId)] = Guid.NewGuid(), [nameof(Lesson.Phases)] = new BsonArray { new BsonDocument { ["AlternativePath"] = "Kurzversion" }, new BsonDocument { ["AlternativePath"] = "" }, // Hauptweg, kein Katalogeintrag new BsonDocument { }, // gar kein Feld, unangetastet }, }); legacy.GetCollection("lessons").Insert(new BsonDocument { ["_id"] = lesson2Id, [nameof(Lesson.UnitId)] = Guid.NewGuid(), [nameof(Lesson.GroupId)] = Guid.NewGuid(), [nameof(Lesson.Phases)] = new BsonArray { new BsonDocument { ["AlternativePath"] = "Kurzversion" }, // gleicher Name -> gleicher Katalogeintrag }, }); } using (var context = new LiteDbContext(temp.Path)) { var catalog = context.AlternativeLessonPaths.FindAll().ToList(); var entry = Assert.Single(catalog); Assert.Equal("Kurzversion", entry.Name); var lesson1 = context.Lessons.FindById(lesson1Id)!; Assert.Equal(entry.Id, lesson1.Phases[0].AlternativePathId); Assert.Null(lesson1.Phases[1].AlternativePathId); Assert.Null(lesson1.Phases[2].AlternativePathId); var lesson2 = context.Lessons.FindById(lesson2Id)!; Assert.Equal(entry.Id, lesson2.Phases[0].AlternativePathId); // wiederverwendeter Katalogeintrag } using var migrated = new LiteDatabase(temp.Path); var rawLesson1 = migrated.GetCollection("lessons").FindById(lesson1Id); var rawPhases = rawLesson1["Phases"].AsArray; Assert.False(rawPhases[0].AsDocument.ContainsKey("AlternativePath")); } private sealed class TempDatabase : IDisposable { private readonly string _directory = System.IO.Path.Combine( System.IO.Path.GetTempPath(), $"lehrerapp-tests-{Guid.NewGuid():N}"); public string Path { get; } public TempDatabase() { Directory.CreateDirectory(_directory); Path = System.IO.Path.Combine(_directory, "test.db"); } public void Dispose() { if (Directory.Exists(_directory)) Directory.Delete(_directory, recursive: true); } } }