Files
LehrerApp/LehrerApp.Tests/CompetencyCatalogImportServiceTests.cs
T
admin 8495e1b8d0 KI-gestützte Planungsunterstützung (4.5.9) + Kompetenzkatalog-Import (8.1.2)
KI-Unterstützung: neuer Einstellungen-Tab (Anmeldung, Guthaben) und Button im
Planungs-Tab, der Einheiten+Stunden als JSON an ein neues PHP-Backend (ai-backend/)
sendet und die Antwort als prüfbare Vorschlagsliste zurückbringt. Provider-Aufruf,
Guthabenverwaltung und Abrechnung nach echten Token-Kosten laufen serverseitig, der
Desktop-Client sieht nie einen LLM-API-Key. Zentral abgesichert: eine von der KI
zurückgegebene Stunden-Id, die zu keiner echten Lesson der Einheit passt, wird nie
als Update übernommen, sondern immer als neue Stunde behandelt.

Kompetenzkatalog-Import (8.1.2): JSON-Export/Import für Kompetenzkataloge.
2026-08-16 01:31:14 +02:00

199 lines
7.5 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 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);
}
}
}