Neuer Tab "Planung" in GroupDetailView ersetzt den Platzhalter: Einheiten anlegen/bearbeiten/ als Vorlage in andere Gruppe kopieren, Stunden je Einheit mit Verschieben (inkl. Nachrücken der Folgestunden). Stundeneditor als tabellarischer Verlaufsplan (Phase/Dauer/Tätigkeit/ Material/Kurzsymbol je Zeile, Uhrzeit aus optionalem Stundenbeginn abgeleitet) statt eines einzelnen Phase-Felds mit Methoden-/Materialien-Chips — Kurzsymbol als Freitext mit Vorschlägen aus neuem Kürzel-Katalog (Einstellungen) plus bisher verwendeten Werten. Schema-Migrationen v1-v3 überführen bestehende Daten verlustfrei. 4.2.5 bewusst offen gelassen (hängt an Stundenplan, Kapitel 4.3). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
245 lines
9.9 KiB
C#
245 lines
9.9 KiB
C#
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<Subject>("subjects").Insert(new Subject { Name = "mathematik" });
|
|
legacy.GetCollection<BsonDocument>("groups").Insert(new BsonDocument
|
|
{
|
|
["_id"] = groupId,
|
|
[nameof(LearningGroup.Name)] = "Mathe G",
|
|
["Subject"] = "Mathematik",
|
|
[nameof(LearningGroup.SchoolYear)] = "2025/26",
|
|
});
|
|
legacy.GetCollection<BsonDocument>("enrollments").Insert(new BsonDocument
|
|
{
|
|
["_id"] = membershipId,
|
|
[nameof(GroupMembership.StudentId)] = studentId,
|
|
[nameof(GroupMembership.GroupId)] = groupId,
|
|
["SchoolYear"] = "2025/26",
|
|
["EnrolledAt"] = BsonMapper.Global.Serialize(addedOn),
|
|
});
|
|
legacy.GetCollection<BsonDocument>("exams").Insert(new BsonDocument
|
|
{
|
|
["_id"] = examId,
|
|
[nameof(Exam.GroupId)] = groupId,
|
|
["Subject"] = "Mathematik",
|
|
});
|
|
legacy.GetCollection<BsonDocument>("grades").Insert(new BsonDocument
|
|
{
|
|
["_id"] = gradeId,
|
|
[nameof(Grade.StudentId)] = studentId,
|
|
[nameof(Grade.GroupId)] = groupId,
|
|
["SchoolYear"] = "2025/26",
|
|
});
|
|
legacy.GetCollection<BsonDocument>("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<BsonDocument>("groups").FindById(groupId);
|
|
var rawMembership = migrated.GetCollection<BsonDocument>("group_memberships").FindById(membershipId);
|
|
var rawExam = migrated.GetCollection<BsonDocument>("exams").FindById(examId);
|
|
var rawGrade = migrated.GetCollection<BsonDocument>("grades").FindById(gradeId);
|
|
var rawEntry = migrated.GetCollection<BsonDocument>("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<LiteException>(() => 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<LiteException>(() => 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(3, 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(3, 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<BsonDocument>("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<BsonDocument>("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<BsonDocument>("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<BsonDocument>("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<BsonDocument>("lessons").FindById(lessonId);
|
|
var rawPhases = rawLesson["Phases"].AsArray;
|
|
Assert.False(rawPhases[0].AsDocument.ContainsKey("ShorthandFrom"));
|
|
Assert.False(rawPhases[0].AsDocument.ContainsKey("ShorthandTo"));
|
|
}
|
|
|
|
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);
|
|
}
|
|
}
|
|
}
|