Files
nexarch/dms/internal/versioning/restore_test.go
T
patrickandClaude Sonnet 5 64af627495 VER-02: Versionsansicht & Wiederherstellung
versioning.go ergaenzt Restore (stellt eine fruehere Revision ueber
denselben CheckOut/CheckIn-Mechanismus als NEUE Revision wieder her, kein
separater Codepfad) und GetRevisionContent (beliebige, nicht nur die
aktuelle Revision). Revision.CreatedBy ergaenzt fuer Autor-Anzeige.

internal/versioningapi: Liste/Content/Restore als HTTP-Endpunkte.
OwnerChecker ist eine minimale, echte PermissionChecker-Implementierung
(Dokument- oder Revisions-Ersteller) - keine zentrale RBAC-Anbindung fuer
dieses Modul vorhanden (VER-01/VER-05-Praezedenzfall), der Aufrufvertrag
ist aber so geschnitten, dass eine spaetere echte Rollenpruefung diesen
Handler nicht aendern muss.

web/dms-browser: Versionsliste (Autor/Zeitpunkt/Kommentar, Download- und
Wiederherstellen-Aktion je Zeile), nutzt dieselbe bei DOC-10 gegen 10.000
Zeilen bewiesene Virtualisierungs-Logik.

Pruefungen:
1. Wiederherstellung veraendert Historie nicht - alle felder der alten
   revisionen unveraendert, wiederhergestellter inhalt real entschluesselt
   und verglichen
2. 100 revisionen bleiben uebersichtlich - dieselbe bei doc-10 bewiesene
   virtualisierung, weit unterhalb des bewiesenen 10.000er-bereichs
3. berechtigung wird JE revision geprueft - nicht-ersteller sieht
   accessible:false und bekommt 403, ersteller kann zugreifen (200)

Regressionsfrei: go test ./... -p 1 alle pakete PASS, 22/22 vitest-tests.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-17 21:26:15 +02:00

111 lines
3.9 KiB
Go

package versioning
import (
"context"
"strings"
"testing"
"time"
)
// Akzeptanzkriterium 2 + Pflichtpruefung 1: Wiederherstellung erzeugt
// eine neue Revision statt zu ueberschreiben, die Historie bleibt
// unveraendert.
func TestRestore_CreatesNewRevisionWithoutAlteringHistory(t *testing.T) {
env, userA, _ := setupTest(t)
ctx := context.Background()
doc := createDocumentWithFirstRevision(t, env, userA, "fassung eins")
if _, err := CheckOut(ctx, env.pool, doc, userA, time.Minute); err != nil {
t.Fatalf("checkout: %v", err)
}
rev2, err := CheckIn(ctx, env.pool, env.storage, env.crypto, "acme", doc, userA, "zweite fassung", "application/pdf", strings.NewReader("fassung zwei"))
if err != nil {
t.Fatalf("checkin: %v", err)
}
beforeRestore, err := ListRevisions(ctx, env.pool, doc)
if err != nil {
t.Fatalf("ListRevisions (vor restore): %v", err)
}
if len(beforeRestore) != 2 {
t.Fatalf("erwartet 2 revisionen vor restore, habe %d", len(beforeRestore))
}
restored, err := Restore(ctx, env.pool, env.storage, env.crypto, "acme", doc, beforeRestore[0].ID, userA)
if err != nil {
t.Fatalf("Restore: %v", err)
}
if restored.RevisionNumber != 3 {
t.Fatalf("restore haette revision 3 erzeugen sollen, habe %d", restored.RevisionNumber)
}
if restored.Comment == nil || !strings.Contains(*restored.Comment, "Revision 1") {
t.Fatalf("restore-kommentar sollte auf die quell-revision verweisen, hatte: %v", restored.Comment)
}
afterRestore, err := ListRevisions(ctx, env.pool, doc)
if err != nil {
t.Fatalf("ListRevisions (nach restore): %v", err)
}
if len(afterRestore) != 3 {
t.Fatalf("erwartet 3 revisionen nach restore (angehaengt, nicht ueberschrieben), habe %d", len(afterRestore))
}
// Die HISTORIE (revisionen 1 und 2) ist unveraendert - Feldvergleich
// statt "!=", da Comment ein *string ist und ein Pointervergleich
// zweier separater Abfragen faelschlich immer ungleich waere.
for i := 0; i < 2; i++ {
before, after := beforeRestore[i], afterRestore[i]
if before.ID != after.ID || before.ChecksumSHA256 != after.ChecksumSHA256 ||
before.SizeBytes != after.SizeBytes || before.MimeType != after.MimeType ||
!before.CreatedAt.Equal(after.CreatedAt) || derefString(before.Comment) != derefString(after.Comment) {
t.Fatalf("revision %d wurde durch restore veraendert: vorher %+v, nachher %+v", i+1, before, after)
}
}
// Wiederhergestellter Inhalt entspricht tatsaechlich der Quell-Revision.
content, _, err := GetRevisionContent(ctx, env.pool, env.storage, env.crypto, "acme", doc, restored.ID)
if err != nil {
t.Fatalf("GetRevisionContent (restored): %v", err)
}
if string(content) != "fassung eins" {
t.Fatalf("wiederhergestellter inhalt = %q, want %q", content, "fassung eins")
}
_ = rev2 // nur zur Illustration der Zwischen-Revision verwendet
}
func derefString(s *string) string {
if s == nil {
return ""
}
return *s
}
// Akzeptanzkriterium 3: jede Revision ist einzeln herunterladbar.
func TestGetRevisionContent_EachRevisionIndividuallyRetrievable(t *testing.T) {
env, userA, _ := setupTest(t)
ctx := context.Background()
doc := createDocumentWithFirstRevision(t, env, userA, "eins")
if _, err := CheckOut(ctx, env.pool, doc, userA, time.Minute); err != nil {
t.Fatalf("checkout: %v", err)
}
if _, err := CheckIn(ctx, env.pool, env.storage, env.crypto, "acme", doc, userA, "zwei", "application/pdf", strings.NewReader("zwei")); err != nil {
t.Fatalf("checkin: %v", err)
}
revisions, err := ListRevisions(ctx, env.pool, doc)
if err != nil {
t.Fatalf("ListRevisions: %v", err)
}
expected := []string{"eins", "zwei"}
for i, rev := range revisions {
content, _, err := GetRevisionContent(ctx, env.pool, env.storage, env.crypto, "acme", doc, rev.ID)
if err != nil {
t.Fatalf("GetRevisionContent(%d): %v", i, err)
}
if string(content) != expected[i] {
t.Fatalf("revision %d inhalt = %q, want %q", i+1, content, expected[i])
}
}
}