Files
LehrerApp/LehrerApp.Tests/CompetencyCatalogImportServiceTests.cs
T
adminandClaude Sonnet 5 3f8813df51 feat: MCP-Tools für Fach-/Kompetenzverwaltung + Kompetenzzuordnung zu Lessons (Nutzer-Nachtrag)
Neue CompetencyTools.cs: create/update/delete_subject, create/update/delete_competency_domain,
add/update/remove_competency_item, get_subjects, get_competency_catalog. delete_subject und
delete_competency_domain als weitere Destructive-Ausnahmen neben delete_lesson (kein Papierkorb).

Lesson bekommt ein neues Competencies-Feld (analog zu Unit.Competencies) sowie
add_lesson_competency/remove_lesson_competency in LessonPlanTools.cs.

ICompetencyDomainRepository um GetBySubject() erweitert, um den Katalog eines Fachs ohne
bekannte Klassenstufe abzufragen.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-12 20:01:23 +02:00

201 lines
7.6 KiB
C#

using LehrerApp.Core.Interfaces;
using LehrerApp.Core.Models;
using LehrerApp.Core.Services;
using Xunit;
namespace LehrerApp.Tests;
public sealed class CompetencyCatalogImportServiceTests
{
private static readonly Guid SubjectId = Guid.NewGuid();
[Fact]
public void Analyze_UngueltigeDatei_VeraendertDenKatalogNicht()
{
var repository = RepositoryWithExistingCatalog();
var service = new CompetencyCatalogImportService(repository);
Assert.Throws<InvalidDataException>(() =>
service.Analyze("{ kaputt", SubjectId, "Mathematik", 7));
Assert.False(repository.ReplaceWasCalled);
Assert.Equal("Alte Beschreibung", repository.Current.Single().Items.Single().Description);
}
[Fact]
public void Analyze_ErkenntNeueUnveraenderteUndKonflikte()
{
var service = new CompetencyCatalogImportService(RepositoryWithExistingCatalog());
var preview = service.Analyze(JsonWithConflictAndNewItem(), SubjectId, "Mathematik", 7);
Assert.Equal(0, preview.NewDomains);
Assert.Equal(1, preview.NewCompetencies);
Assert.Single(preview.Conflicts);
Assert.Equal("item:A1", preview.Conflicts[0].Id);
}
[Fact]
public void Apply_MergeBehaeltBeiKonfliktStandardmaessigDenVorhandenenWert()
{
var repository = RepositoryWithExistingCatalog();
var service = new CompetencyCatalogImportService(repository);
var preview = service.Analyze(JsonWithConflictAndNewItem(), SubjectId, "Mathematik", 7);
service.Apply(preview, CompetencyCatalogImportMode.Merge);
Assert.True(repository.ReplaceWasCalled);
var items = repository.Current.Single().Items;
Assert.Equal("Alte Beschreibung", items.Single(x => x.Code == "A1").Description);
Assert.Equal("Neue Kompetenz", items.Single(x => x.Code == "A2").Description);
}
[Fact]
public void Apply_MergeKannImportiertenKonfliktwertUebernehmen()
{
var repository = RepositoryWithExistingCatalog();
var service = new CompetencyCatalogImportService(repository);
var preview = service.Analyze(JsonWithConflictAndNewItem(), SubjectId, "Mathematik", 7);
service.Apply(preview, CompetencyCatalogImportMode.Merge, new HashSet<string> { "item:A1" });
Assert.Equal("Importierte Beschreibung",
repository.Current.Single().Items.Single(x => x.Code == "A1").Description);
}
[Fact]
public void Apply_NeuerBereichMitVorhandenemCode_ErzeugtKeinenDoppeltenKompetenzcode()
{
var repository = RepositoryWithExistingCatalog();
var service = new CompetencyCatalogImportService(repository);
const string json = """
{ "domains": [
{ "code": "B", "name": "Neuer Bereich", "competencies": [
{ "code": "A1", "description": "Andere Fassung" }
] }
] }
""";
var preview = service.Analyze(json, SubjectId, "Mathematik", 7);
service.Apply(preview, CompetencyCatalogImportMode.Merge);
Assert.Single(repository.Current.SelectMany(x => x.Items), x => x.Code == "A1");
Assert.Equal("Alte Beschreibung",
repository.Current.SelectMany(x => x.Items).Single(x => x.Code == "A1").Description);
}
[Fact]
public void Apply_ReplaceErsetztDenGesamtenAusgewaehltenKatalog()
{
var repository = RepositoryWithExistingCatalog();
var service = new CompetencyCatalogImportService(repository);
var preview = service.Analyze(JsonWithConflictAndNewItem(), SubjectId, "Mathematik", 7);
service.Apply(preview, CompetencyCatalogImportMode.Replace);
Assert.Equal(2, repository.Current.Single().Items.Count);
Assert.Equal("Importierte Beschreibung",
repository.Current.Single().Items.Single(x => x.Code == "A1").Description);
}
[Fact]
public void Apply_NachZwischenzeitlicherAenderung_VerlangtNeueVorschau()
{
var repository = RepositoryWithExistingCatalog();
var service = new CompetencyCatalogImportService(repository);
var preview = service.Analyze(JsonWithConflictAndNewItem(), SubjectId, "Mathematik", 7);
repository.Current[0].UpdatedAt = repository.Current[0].UpdatedAt.AddSeconds(1);
var exception = Assert.Throws<InvalidOperationException>(() =>
service.Apply(preview, CompetencyCatalogImportMode.Merge));
Assert.Contains("Vorschau", exception.Message);
Assert.False(repository.ReplaceWasCalled);
}
[Fact]
public void Analyze_AbweichendesFachUndKlassenstufe_ErzeugtWarnungen()
{
var service = new CompetencyCatalogImportService(RepositoryWithExistingCatalog());
var json = JsonWithConflictAndNewItem()
.Replace("Mathematik", "Deutsch")
.Replace("\"gradeLevel\": 7", "\"gradeLevel\": 8");
var preview = service.Analyze(json, SubjectId, "Mathematik", 7);
Assert.Equal(2, preview.Warnings.Count);
}
[Fact]
public void Analyze_DoppelterKompetenzcode_WirdAbgelehnt()
{
var service = new CompetencyCatalogImportService(RepositoryWithExistingCatalog());
const string json = """
{ "domains": [
{ "name": "A", "competencies": [{ "code": "X1", "description": "Eins" }] },
{ "name": "B", "competencies": [{ "code": "X1", "description": "Zwei" }] }
] }
""";
Assert.Throws<InvalidDataException>(() =>
service.Analyze(json, SubjectId, "Mathematik", 7));
}
private static InMemoryCompetencyDomainRepository RepositoryWithExistingCatalog() => new(
[
new CompetencyDomain
{
SubjectId = SubjectId,
GradeLevel = 7,
Code = "A",
Name = "Arithmetik",
Items =
[
new CompetencyItem { Code = "A1", Description = "Alte Beschreibung" },
],
},
]);
private static string JsonWithConflictAndNewItem() => """
{
"subject": "Mathematik",
"gradeLevel": 7,
"domains": [
{
"code": "A",
"name": "Arithmetik",
"competencies": [
{ "code": "A1", "description": "Importierte Beschreibung" },
{ "code": "A2", "description": "Neue Kompetenz" }
]
}
]
}
""";
private sealed class InMemoryCompetencyDomainRepository(List<CompetencyDomain> initial)
: ICompetencyDomainRepository
{
public List<CompetencyDomain> Current { get; private set; } = initial;
public bool ReplaceWasCalled { get; private set; }
public List<CompetencyDomain> GetBySubjectAndGrade(Guid subjectId, int gradeLevel) =>
Current.Where(x => x.SubjectId == subjectId && x.GradeLevel == gradeLevel).ToList();
public List<CompetencyDomain> GetBySubject(Guid subjectId) =>
Current.Where(x => x.SubjectId == subjectId).ToList();
public CompetencyDomain? GetById(Guid id) => Current.FirstOrDefault(x => x.Id == id);
public void Save(CompetencyDomain domain) => Current.Add(domain);
public void Delete(Guid id) => Current.RemoveAll(x => x.Id == id);
public void DeleteBySubjectAndGrade(Guid subjectId, int gradeLevel) =>
Current.RemoveAll(x => x.SubjectId == subjectId && x.GradeLevel == gradeLevel);
public void ReplaceForSubjectAndGrade(Guid subjectId, int gradeLevel, List<CompetencyDomain> domains)
{
ReplaceWasCalled = true;
Current.RemoveAll(x => x.SubjectId == subjectId && x.GradeLevel == gradeLevel);
Current.AddRange(domains);
}
}
}