feat: Anhänge je Stunde, Klausur-Sitzplan mischen, Gefährdungsbeurteilungs-Assistent
Lesson bekommt dieselbe Anhang-Infrastruktur wie Documentation (Material, Arbeitsblätter, Experimentunterlagen), samt Fix einer Sync-Lücke, die Anhang- Dateibytes bisher nur für Documentation statt generisch übertragen hat (IHasAttachments). Sitzplan-Tab bekommt einen "Plätze mischen"-Button für Klausursitzpläne. Neu: mehrschrittiger Gefährdungsbeurteilungs-Assistent mit optionalem KI-Entwurf (ai-backend/gbu.php) und PDF-Export, Format bewusst als JSON-Anhang statt eigener Datenbank-Entität. Details und Architekturentscheidungen in TODO.md (4.2, 7.1.5, 10.1.8). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -180,6 +180,32 @@ public sealed class EventApplierTests
|
||||
Assert.Empty(handler.Requests);
|
||||
}
|
||||
|
||||
/// Regressionstest für die Verallgemeinerung von "nur Documentation" auf
|
||||
/// <see cref="IHasAttachments"/> — Anhänge an einer <see cref="Lesson"/> (Material,
|
||||
/// Gefährdungsbeurteilung) müssen genauso nachgeladen werden.
|
||||
[Fact]
|
||||
public async Task ApplyAsync_LessonMitFehlendemAnhang_LaedtIhnUeberHttpNach()
|
||||
{
|
||||
using var db = NewInMemoryContext();
|
||||
var storageId = Guid.NewGuid().ToString("N");
|
||||
var handler = new FakeHttpMessageHandler(req =>
|
||||
{
|
||||
Assert.Equal($"/api/sync/attachments/{storageId}", req.RequestUri!.AbsolutePath);
|
||||
var encrypted = SyncCrypto.Encrypt([9, 8, 7], Key);
|
||||
return new HttpResponseMessage(System.Net.HttpStatusCode.OK)
|
||||
{ Content = new ByteArrayContent(encrypted) };
|
||||
});
|
||||
var http = new HttpClient(handler) { BaseAddress = new Uri("https://example.invalid") };
|
||||
var applier = new EventApplier(db, Key, http);
|
||||
var lesson = new Lesson { UnitId = Guid.NewGuid(), GroupId = Guid.NewGuid(), Topic = "Brechung" };
|
||||
lesson.Attachments.Add(new DocumentAttachment { StorageId = storageId, FileName = "gbu.pdf", SizeBytes = 3 });
|
||||
|
||||
await applier.ApplyAsync(MakeEvent(nameof(Lesson), lesson.Id.ToString(), "Save", lesson));
|
||||
|
||||
Assert.Single(handler.Requests);
|
||||
Assert.True(db.Attachments.Exists(storageId));
|
||||
}
|
||||
|
||||
private static SyncEvent MakeEvent(string entityType, string entityId, string operation, object? payload) => new()
|
||||
{
|
||||
DeviceId = "companion-1",
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
using LehrerApp.Core.Models;
|
||||
using LehrerApp.Sync.Crypto;
|
||||
using Xunit;
|
||||
|
||||
namespace LehrerApp.Sync.Tests;
|
||||
|
||||
public sealed class SyncEventPublisherTests
|
||||
{
|
||||
private static readonly byte[] Key = SyncCrypto.GenerateKey();
|
||||
|
||||
[Fact]
|
||||
public void Publish_DocumentationMitAnhang_ReihtIhnZumHochladenEin()
|
||||
{
|
||||
using var temp = new TempEventQueue();
|
||||
var publisher = new SyncEventPublisher(temp.Queue, "desktop-1", Key);
|
||||
var doc = new Documentation { StudentId = Guid.NewGuid(), Title = "Elternbrief" };
|
||||
doc.Attachments.Add(new DocumentAttachment { StorageId = "abc", FileName = "brief.pdf" });
|
||||
|
||||
publisher.Publish(nameof(Documentation), doc.Id.ToString(), "Save", doc);
|
||||
|
||||
Assert.Equal(["abc"], temp.Queue.GetPendingAttachmentUploads());
|
||||
}
|
||||
|
||||
/// Regressionstest für die Verallgemeinerung von "nur Documentation" auf
|
||||
/// <see cref="IHasAttachments"/> — Anhänge an einer <see cref="Lesson"/> müssen genauso in die
|
||||
/// Upload-Warteliste eingereiht werden.
|
||||
[Fact]
|
||||
public void Publish_LessonMitAnhang_ReihtIhnZumHochladenEin()
|
||||
{
|
||||
using var temp = new TempEventQueue();
|
||||
var publisher = new SyncEventPublisher(temp.Queue, "desktop-1", Key);
|
||||
var lesson = new Lesson { UnitId = Guid.NewGuid(), GroupId = Guid.NewGuid(), Topic = "Brechung" };
|
||||
lesson.Attachments.Add(new DocumentAttachment { StorageId = "gbu-1", FileName = "gbu.pdf" });
|
||||
|
||||
publisher.Publish(nameof(Lesson), lesson.Id.ToString(), "Save", lesson);
|
||||
|
||||
Assert.Equal(["gbu-1"], temp.Queue.GetPendingAttachmentUploads());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Publish_EntitaetOhneAnhaenge_ReihtNichtsZumHochladenEin()
|
||||
{
|
||||
using var temp = new TempEventQueue();
|
||||
var publisher = new SyncEventPublisher(temp.Queue, "desktop-1", Key);
|
||||
var student = new Student { FirstName = "Anna", LastName = "Beispiel" };
|
||||
|
||||
publisher.Publish(nameof(Student), student.Id.ToString(), "Save", student);
|
||||
|
||||
Assert.Empty(temp.Queue.GetPendingAttachmentUploads());
|
||||
}
|
||||
|
||||
private sealed class TempEventQueue : IDisposable
|
||||
{
|
||||
private readonly string _directory = Path.Combine(
|
||||
Path.GetTempPath(), $"lehrerapp-sync-tests-publisher-{Guid.NewGuid():N}");
|
||||
public EventQueue Queue { get; }
|
||||
|
||||
public TempEventQueue()
|
||||
{
|
||||
Directory.CreateDirectory(_directory);
|
||||
Queue = new EventQueue(Path.Combine(_directory, "queue.db"));
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Queue.Dispose();
|
||||
if (Directory.Exists(_directory)) Directory.Delete(_directory, recursive: true);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user