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>
111 lines
3.9 KiB
Go
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])
|
|
}
|
|
}
|
|
}
|