Kapitel 7 abgeschlossen.
This commit is contained in:
@@ -0,0 +1,110 @@
|
||||
using LehrerApp.Core.Interfaces;
|
||||
using LehrerApp.Core.Models;
|
||||
using LehrerApp.Core.Services;
|
||||
using Xunit;
|
||||
|
||||
namespace LehrerApp.Tests;
|
||||
|
||||
public sealed class GroupRolloverServiceTests
|
||||
{
|
||||
[Fact]
|
||||
public void RollOver_KopiertStammdatenUndErzeugtNeueGanzjaehrigeMitgliedschaften()
|
||||
{
|
||||
var source = new LearningGroup
|
||||
{
|
||||
Name = "8a", SchoolYear = "2025/26", GradeLevel = 8,
|
||||
Type = GroupType.Class, SubjectId = Guid.NewGuid(), GradingSystem = GradingSystem.Grades1To6,
|
||||
HoursPerWeek = 4, IsOwnClass = true, IsDifferentiated = true,
|
||||
};
|
||||
var oldMembership = new GroupMembership
|
||||
{
|
||||
StudentId = Guid.NewGuid(), GroupId = source.Id, Period = MembershipPeriod.H2Only,
|
||||
JoinedAt = new DateOnly(2026, 2, 1), LeftAt = new DateOnly(2026, 7, 31), Niveau = Niveau.E,
|
||||
};
|
||||
var groups = new MemoryGroups([source]);
|
||||
var memberships = new MemoryMemberships([oldMembership]);
|
||||
var schemes = new MemorySchemes();
|
||||
schemes.Save(new GradingScheme
|
||||
{
|
||||
GroupId = source.Id, ExamsPercent = 40, ParticipationPercent = 50, OtherPercent = 10,
|
||||
});
|
||||
var service = new GroupRolloverService(groups, memberships, schemes, new SchoolYearService());
|
||||
|
||||
var target = service.RollOver(source, new GroupRolloverRequest(
|
||||
"9a", "2026/27", 9, [oldMembership.Id], true, true));
|
||||
|
||||
Assert.Equal("9a", target.Name);
|
||||
Assert.Equal(source.SubjectId, target.SubjectId);
|
||||
Assert.Equal(source.HoursPerWeek, target.HoursPerWeek);
|
||||
Assert.True(target.IsOwnClass);
|
||||
Assert.True(target.IsDifferentiated);
|
||||
Assert.False(source.IsActive);
|
||||
|
||||
var copied = Assert.Single(memberships.GetByGroup(target.Id));
|
||||
Assert.NotEqual(oldMembership.Id, copied.Id);
|
||||
Assert.Equal(MembershipPeriod.FullYear, copied.Period);
|
||||
Assert.Equal(new DateOnly(2026, 8, 1), copied.JoinedAt);
|
||||
Assert.Null(copied.LeftAt);
|
||||
Assert.Equal(Niveau.E, copied.Niveau);
|
||||
|
||||
var scheme = schemes.GetByGroup(target.Id);
|
||||
Assert.NotNull(scheme);
|
||||
Assert.Equal(40, scheme.ExamsPercent);
|
||||
Assert.NotEqual(schemes.GetByGroup(source.Id)!.Id, scheme.Id);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RollOver_GleichnamigeGruppeImZieljahr_WirdAbgelehnt()
|
||||
{
|
||||
var subjectId = Guid.NewGuid();
|
||||
var source = new LearningGroup
|
||||
{
|
||||
Name = "8a", SchoolYear = "2025/26", GradeLevel = 8,
|
||||
Type = GroupType.Class, SubjectId = subjectId,
|
||||
};
|
||||
var existing = new LearningGroup
|
||||
{
|
||||
Name = "9a", SchoolYear = "2026/27", GradeLevel = 9,
|
||||
Type = GroupType.Class, SubjectId = subjectId,
|
||||
};
|
||||
var membership = new GroupMembership { StudentId = Guid.NewGuid(), GroupId = source.Id };
|
||||
var service = new GroupRolloverService(
|
||||
new MemoryGroups([source, existing]), new MemoryMemberships([membership]),
|
||||
new MemorySchemes(), new SchoolYearService());
|
||||
|
||||
var error = Assert.Throws<InvalidOperationException>(() => service.RollOver(source,
|
||||
new GroupRolloverRequest("9a", "2026/27", 9, [membership.Id], false, false)));
|
||||
|
||||
Assert.Contains("bereits", error.Message);
|
||||
}
|
||||
|
||||
private sealed class MemoryGroups(List<LearningGroup> items) : IGroupRepository
|
||||
{
|
||||
public LearningGroup? GetById(Guid id) => items.FirstOrDefault(g => g.Id == id);
|
||||
public List<LearningGroup> GetAll(bool includeInactive = false) =>
|
||||
items.Where(g => includeInactive || g.IsActive).ToList();
|
||||
public List<LearningGroup> GetBySchoolYear(string schoolYear, bool includeInactive = false) =>
|
||||
items.Where(g => g.SchoolYear == schoolYear && (includeInactive || g.IsActive)).ToList();
|
||||
public void Save(LearningGroup group) { items.RemoveAll(g => g.Id == group.Id); items.Add(group); }
|
||||
public void Delete(Guid id) => items.RemoveAll(g => g.Id == id);
|
||||
}
|
||||
|
||||
private sealed class MemoryMemberships(List<GroupMembership> items) : IGroupMembershipRepository
|
||||
{
|
||||
public List<GroupMembership> GetByStudent(Guid studentId) => items.Where(m => m.StudentId == studentId).ToList();
|
||||
public List<GroupMembership> GetByGroup(Guid groupId) => items.Where(m => m.GroupId == groupId).ToList();
|
||||
public GroupMembership? GetByStudentAndGroup(Guid studentId, Guid groupId) =>
|
||||
items.FirstOrDefault(m => m.StudentId == studentId && m.GroupId == groupId);
|
||||
public void Save(GroupMembership membership) { items.RemoveAll(m => m.Id == membership.Id); items.Add(membership); }
|
||||
public void Delete(Guid id) => items.RemoveAll(m => m.Id == id);
|
||||
}
|
||||
|
||||
private sealed class MemorySchemes : IGradingSchemeRepository
|
||||
{
|
||||
private readonly List<GradingScheme> _items = [];
|
||||
public GradingScheme? GetByGroup(Guid groupId) => _items.FirstOrDefault(s => s.GroupId == groupId);
|
||||
public GradingScheme? GetDefaultForType(GroupType type) => _items.FirstOrDefault(s => s.GroupType == type);
|
||||
public void Save(GradingScheme scheme) { _items.RemoveAll(s => s.Id == scheme.Id); _items.Add(scheme); }
|
||||
public void Delete(Guid id) => _items.RemoveAll(s => s.Id == id);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
using System.IO.Compression;
|
||||
using System.Xml.Linq;
|
||||
using LehrerApp.Core.Services;
|
||||
using Xunit;
|
||||
|
||||
namespace LehrerApp.Tests;
|
||||
|
||||
public sealed class LetterTemplateServiceTests : IDisposable
|
||||
{
|
||||
private static readonly XNamespace W = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
|
||||
private readonly string _directory = Path.Combine(Path.GetTempPath(), $"lehrerapp-letter-tests-{Guid.NewGuid():N}");
|
||||
|
||||
public LetterTemplateServiceTests() => Directory.CreateDirectory(_directory);
|
||||
|
||||
[Fact]
|
||||
public void Validate_OhneInhaltssteuerelement_Warnt()
|
||||
{
|
||||
var path = CreateDocx("<w:p><w:r><w:t>Brief</w:t></w:r></w:p>");
|
||||
|
||||
var result = new LetterTemplateService(_directory).Validate(path);
|
||||
|
||||
Assert.Contains(result.Issues, i => i.Severity == TemplateIssueSeverity.Warning
|
||||
&& i.Message.Contains("keine Inhaltssteuerelemente"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_UnbekannterTag_Warnt()
|
||||
{
|
||||
var path = CreateDocx(Control("Something.EntirelyUnknown"));
|
||||
|
||||
var result = new LetterTemplateService(_directory).Validate(path);
|
||||
|
||||
Assert.Contains(result.Issues, i => i.Tag == "Something.EntirelyUnknown"
|
||||
&& i.Severity == TemplateIssueSeverity.Warning);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_WahrscheinlicherTippfehler_WarntDeutlichMitVorschlag()
|
||||
{
|
||||
var path = CreateDocx(Control("Contact.Adress"));
|
||||
|
||||
var result = new LetterTemplateService(_directory).Validate(path);
|
||||
|
||||
var issue = Assert.Single(result.Issues);
|
||||
Assert.Equal(TemplateIssueSeverity.StrongWarning, issue.Severity);
|
||||
Assert.Equal("Contact.Address", issue.SuggestedTag);
|
||||
Assert.Contains("Meinten Sie", issue.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_FindetTagsInKopfzeile()
|
||||
{
|
||||
var path = CreateDocx(Control("Student.FirstName"), Control("CurrentDate"));
|
||||
|
||||
var result = new LetterTemplateService(_directory).Validate(path);
|
||||
|
||||
Assert.Contains("Student.FirstName", result.Tags);
|
||||
Assert.Contains("CurrentDate", result.Tags);
|
||||
Assert.Empty(result.Issues);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Generate_BefuelltTextUndMehrzeiligeAdresse_OhneVorlageZuVeraendern()
|
||||
{
|
||||
var path = CreateDocx(Control("Student.FirstName") + Control("Contact.Address"));
|
||||
var original = File.ReadAllBytes(path);
|
||||
var output = Path.Combine(_directory, "filled.docx");
|
||||
var service = new LetterTemplateService(_directory);
|
||||
|
||||
service.Generate(path, output, new Dictionary<string, string?>
|
||||
{
|
||||
["Student.FirstName"] = "Mara",
|
||||
["Contact.Address"] = "Hauptstraße 1\n12345 Musterstadt",
|
||||
});
|
||||
|
||||
Assert.Equal(original, File.ReadAllBytes(path));
|
||||
using var archive = ZipFile.OpenRead(output);
|
||||
var entry = archive.GetEntry("word/document.xml")!;
|
||||
using var reader = new StreamReader(entry.Open());
|
||||
var xml = XDocument.Parse(reader.ReadToEnd());
|
||||
Assert.Contains("Mara", xml.Descendants(W + "t").Select(t => t.Value));
|
||||
Assert.Single(xml.Descendants(W + "br"));
|
||||
Assert.Contains("12345 Musterstadt", xml.Descendants(W + "t").Select(t => t.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Import_KopiertVorlageUndLoeschenEntferntSie()
|
||||
{
|
||||
var path = CreateDocx(Control("CurrentDate"));
|
||||
var service = new LetterTemplateService(Path.Combine(_directory, "appdata"));
|
||||
|
||||
var imported = service.Import(path, "Elternbrief");
|
||||
|
||||
Assert.Equal("Elternbrief", Assert.Single(service.GetTemplates()).Name);
|
||||
Assert.True(File.Exists(service.GetTemplatePath(imported)));
|
||||
service.Delete(imported.Id);
|
||||
Assert.Empty(service.GetTemplates());
|
||||
Assert.False(File.Exists(service.GetTemplatePath(imported)));
|
||||
}
|
||||
|
||||
private string CreateDocx(string body, string? header = null)
|
||||
{
|
||||
var path = Path.Combine(_directory, $"{Guid.NewGuid():N}.docx");
|
||||
using var archive = ZipFile.Open(path, ZipArchiveMode.Create);
|
||||
AddXml(archive, "word/document.xml", body);
|
||||
if (header is not null) AddXml(archive, "word/header1.xml", header);
|
||||
return path;
|
||||
}
|
||||
|
||||
private static void AddXml(ZipArchive archive, string name, string content)
|
||||
{
|
||||
var entry = archive.CreateEntry(name);
|
||||
using var writer = new StreamWriter(entry.Open());
|
||||
writer.Write($"<w:document xmlns:w=\"{W}\"><w:body>{content}</w:body></w:document>");
|
||||
}
|
||||
|
||||
private static string Control(string tag) =>
|
||||
$"<w:sdt><w:sdtPr><w:tag w:val=\"{tag}\"/></w:sdtPr><w:sdtContent>" +
|
||||
"<w:p><w:r><w:rPr><w:b/></w:rPr><w:t>Platzhalter</w:t></w:r></w:p>" +
|
||||
"</w:sdtContent></w:sdt>";
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (Directory.Exists(_directory)) Directory.Delete(_directory, recursive: true);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user