Files
LehrerApp/LehrerApp.Tests/CompetencyCatalogImportServiceTests.cs
T
adminandClaude Sonnet 5 f97edff2fe feat: Bildungsplan-Versionierung für Kompetenzkataloge, vollständig über MCP exponiert
Neue Bildungspläne wachsen je Fach klassenstufenweise hoch, sodass Kurse abhängig vom
Einschulungsjahr entweder dem alten oder dem neuen Bildungsplan folgen. CompetencyCatalogVersion
(Fach + Name + IsDefault) macht das abbildbar: CompetencyDomain bekommt eine VersionId, Kurse
wählen über LearningGroup.CompetencyCatalogVersionId (null = Standardversion des Fachs) welche
Version gilt. Migration legt für bestehende Kataloge automatisch eine Standardversion an, damit
alte Datenbanken unverändert weiterlaufen.

MCP: neue Tools für Versions-CRUD (get/create/update/delete_competency_plan_version,
update_group_competency_version), get_competency_catalog/create_competency_domain um optionalen
versionId-Parameter erweitert.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-19 00:07:29 +02:00

202 lines
8.1 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, Guid.Empty));
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, Guid.Empty);
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, Guid.Empty);
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, Guid.Empty);
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, Guid.Empty);
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, Guid.Empty);
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, Guid.Empty);
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, Guid.Empty);
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, Guid.Empty));
}
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, Guid versionId) =>
Current.Where(x => x.SubjectId == subjectId && x.GradeLevel == gradeLevel && x.VersionId == versionId).ToList();
public List<CompetencyDomain> GetBySubjectAndVersion(Guid subjectId, Guid versionId) =>
Current.Where(x => x.SubjectId == subjectId && x.VersionId == versionId).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 DeleteBySubjectGradeAndVersion(Guid subjectId, int gradeLevel, Guid versionId) =>
Current.RemoveAll(x => x.SubjectId == subjectId && x.GradeLevel == gradeLevel && x.VersionId == versionId);
public void ReplaceForSubjectGradeAndVersion(Guid subjectId, int gradeLevel, Guid versionId, List<CompetencyDomain> domains)
{
ReplaceWasCalled = true;
Current.RemoveAll(x => x.SubjectId == subjectId && x.GradeLevel == gradeLevel && x.VersionId == versionId);
foreach (var d in domains) { d.SubjectId = subjectId; d.GradeLevel = gradeLevel; d.VersionId = versionId; }
Current.AddRange(domains);
}
}
}