Unit/Lesson/ExamTask/ParticipationSession referenzierten Kompetenzen bisher per Freitext-Code, case-insensitiv gegen den Katalog gematcht - fragil bei Umbenennungen und seit der Bildungsplan-Versionierung nicht mehr eindeutig (zwei Versionen können denselben Code vergeben). Neuer Werttyp CompetencyReference (ItemId + Code/Description-Momentaufnahme) löst das: solange die Id noch im Katalog auflösbar ist, zählt die live Fassung (Rename wirkt sofort), sonst bleibt die gespeicherte historische Fassung sichtbar statt spurlos zu verschwinden. Migration (Schema-Version 6) löst bestehende Codes gegen den Katalog auf; die zuvor dreifach duplizierte Kompetenz-Tag-UI ist jetzt in CompetencyTagGroupBuilder konsolidiert. add_lesson_competency/remove_lesson_competency nehmen jetzt itemId statt code und validieren gegen den Katalog (vorher ungeprüft). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
203 lines
8.1 KiB
C#
203 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 List<CompetencyDomain> GetAll() => Current.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);
|
|
}
|
|
}
|
|
}
|