Deterministischer Abgleich zwischen DMS file_revisions und Objekt-Storage-Verzeichnis, existenz-only (keine Inhaltspruefung, saubere Abgrenzung zu BAK-08). Report sortiert nach storage_key fuer stabile Weiterverarbeitung durch BAK-08. Systemd-Timer taeglich, real auf 192.168.1.131 verdrahtet und ausgeloest.
138 lines
5.7 KiB
Go
138 lines
5.7 KiB
Go
package reconcile
|
|
|
|
import "testing"
|
|
|
|
// TestReconcile_DetectsMissingInStorage ist Akzeptanzkriterium 1 / Pruefung
|
|
// 1: ein Datenbankeintrag ohne zugehoeriges Objekt im Storage wird erkannt.
|
|
func TestReconcile_DetectsMissingInStorage(t *testing.T) {
|
|
db := []DBEntry{
|
|
{StorageKey: "documents/d1/revisions/r1", DocumentID: "d1", RevisionID: "r1"},
|
|
{StorageKey: "documents/d2/revisions/r1", DocumentID: "d2", RevisionID: "r1"},
|
|
}
|
|
storage := []string{"documents/d1/revisions/r1"} // d2/r1 fehlt absichtlich
|
|
|
|
report := Reconcile(db, storage)
|
|
|
|
if len(report.MissingInStorage) != 1 {
|
|
t.Fatalf("erwartet 1 fund in missing_in_storage, habe %d: %+v", len(report.MissingInStorage), report.MissingInStorage)
|
|
}
|
|
if report.MissingInStorage[0].StorageKey != "documents/d2/revisions/r1" {
|
|
t.Fatalf("unerwarteter fund: %+v", report.MissingInStorage[0])
|
|
}
|
|
if len(report.OrphanedInStorage) != 0 {
|
|
t.Fatalf("erwartet 0 funde in orphaned_in_storage, habe %d", len(report.OrphanedInStorage))
|
|
}
|
|
}
|
|
|
|
// TestReconcile_DetectsOrphanedInStorage ist Akzeptanzkriterium 2 /
|
|
// Pruefung 2: ein Storage-Objekt ohne Datenbankeintrag wird erkannt.
|
|
func TestReconcile_DetectsOrphanedInStorage(t *testing.T) {
|
|
db := []DBEntry{
|
|
{StorageKey: "documents/d1/revisions/r1", DocumentID: "d1", RevisionID: "r1"},
|
|
}
|
|
storage := []string{
|
|
"documents/d1/revisions/r1",
|
|
"documents/verwaist/revisions/r1", // kein DB-Eintrag dafuer
|
|
}
|
|
|
|
report := Reconcile(db, storage)
|
|
|
|
if len(report.OrphanedInStorage) != 1 {
|
|
t.Fatalf("erwartet 1 fund in orphaned_in_storage, habe %d: %+v", len(report.OrphanedInStorage), report.OrphanedInStorage)
|
|
}
|
|
if report.OrphanedInStorage[0].StorageKey != "documents/verwaist/revisions/r1" {
|
|
t.Fatalf("unerwarteter fund: %+v", report.OrphanedInStorage[0])
|
|
}
|
|
if len(report.MissingInStorage) != 0 {
|
|
t.Fatalf("erwartet 0 funde in missing_in_storage, habe %d", len(report.MissingInStorage))
|
|
}
|
|
}
|
|
|
|
// TestReconcile_CleanRunProducesEmptyReport ist Pruefung 3: Lauf ohne
|
|
// Abweichungen liefert einen leeren, eindeutig als sauber erkennbaren
|
|
// Bericht.
|
|
func TestReconcile_CleanRunProducesEmptyReport(t *testing.T) {
|
|
db := []DBEntry{
|
|
{StorageKey: "documents/d1/revisions/r1", DocumentID: "d1", RevisionID: "r1"},
|
|
{StorageKey: "documents/d2/revisions/r1", DocumentID: "d2", RevisionID: "r1"},
|
|
}
|
|
storage := []string{"documents/d1/revisions/r1", "documents/d2/revisions/r1"}
|
|
|
|
report := Reconcile(db, storage)
|
|
|
|
if !report.IsClean() {
|
|
t.Fatalf("erwartet sauberen bericht, habe missing=%v orphaned=%v", report.MissingInStorage, report.OrphanedInStorage)
|
|
}
|
|
if len(report.MissingInStorage) != 0 || len(report.OrphanedInStorage) != 0 {
|
|
t.Fatal("IsClean()==true, aber listen sind nicht leer - widerspruch")
|
|
}
|
|
}
|
|
|
|
// TestReconcile_ExistingButCorruptedObjectProducesNoFinding ist der
|
|
// Nachweis, dass BAK-05 AUSSCHLIESSLICH Existenz prueft, niemals Inhalt
|
|
// (die Fehlerklasse "existiert, aber Inhalt beschaedigt" ist Archive
|
|
// BAK-08, bewusst nicht hier) — Reconcile bekommt nur SCHLUESSEL, hat gar
|
|
// keine Moeglichkeit, auf Inhalt zuzugreifen; dieser Test dokumentiert die
|
|
// Absicht explizit, damit sie nicht versehentlich spaeter aufgeweicht wird.
|
|
func TestReconcile_ExistingButCorruptedObjectProducesNoFinding(t *testing.T) {
|
|
db := []DBEntry{
|
|
{StorageKey: "documents/d1/revisions/r1", DocumentID: "d1", RevisionID: "r1"},
|
|
}
|
|
// "korruptes" Objekt hier rein simuliert durch denselben Schluessel -
|
|
// Reconcile kennt und prueft keinen Inhalt, nur den Schluessel selbst.
|
|
storage := []string{"documents/d1/revisions/r1"}
|
|
|
|
report := Reconcile(db, storage)
|
|
|
|
if !report.IsClean() {
|
|
t.Fatalf("ein existierendes (wenn auch inhaltlich korruptes) objekt haette KEINEN befund ausloesen duerfen, habe: %+v", report)
|
|
}
|
|
}
|
|
|
|
// TestReconcile_DeterministicOrdering ist der Nachweis fuer die
|
|
// Stabilitaets-Anforderung: gleiche Eingabe liefert bei mehreren Laeufen
|
|
// IMMER dieselbe Reihenfolge (Voraussetzung dafuer, dass Archive BAK-08
|
|
// die Liste der existierenden Objekte stabil weiterverarbeiten kann, ohne
|
|
// selbst neu zu sortieren/filtern).
|
|
func TestReconcile_DeterministicOrdering(t *testing.T) {
|
|
db := []DBEntry{
|
|
{StorageKey: "documents/z/revisions/r1", DocumentID: "z", RevisionID: "r1"},
|
|
{StorageKey: "documents/a/revisions/r1", DocumentID: "a", RevisionID: "r1"},
|
|
{StorageKey: "documents/m/revisions/r1", DocumentID: "m", RevisionID: "r1"},
|
|
}
|
|
storage := []string{
|
|
"documents/a/revisions/r1", // deckt genau den DB-Eintrag "a" ab
|
|
"documents/y/revisions/r1",
|
|
"documents/n/revisions/r1",
|
|
}
|
|
|
|
first := Reconcile(db, storage)
|
|
second := Reconcile(db, storage)
|
|
|
|
if len(first.MissingInStorage) != len(second.MissingInStorage) {
|
|
t.Fatal("unterschiedliche anzahl funde zwischen zwei laeufen mit identischer eingabe")
|
|
}
|
|
for i := range first.MissingInStorage {
|
|
if first.MissingInStorage[i].StorageKey != second.MissingInStorage[i].StorageKey {
|
|
t.Fatalf("reihenfolge in missing_in_storage nicht deterministisch: lauf1[%d]=%q lauf2[%d]=%q",
|
|
i, first.MissingInStorage[i].StorageKey, i, second.MissingInStorage[i].StorageKey)
|
|
}
|
|
}
|
|
for i := range first.OrphanedInStorage {
|
|
if first.OrphanedInStorage[i].StorageKey != second.OrphanedInStorage[i].StorageKey {
|
|
t.Fatalf("reihenfolge in orphaned_in_storage nicht deterministisch: lauf1[%d]=%q lauf2[%d]=%q",
|
|
i, first.OrphanedInStorage[i].StorageKey, i, second.OrphanedInStorage[i].StorageKey)
|
|
}
|
|
}
|
|
// Aufsteigend sortiert (a < m < z), nicht Einfuegereihenfolge.
|
|
wantOrder := []string{"documents/m/revisions/r1", "documents/z/revisions/r1"}
|
|
if len(first.MissingInStorage) != len(wantOrder) {
|
|
t.Fatalf("erwartet %d funde, habe %d", len(wantOrder), len(first.MissingInStorage))
|
|
}
|
|
for i, w := range wantOrder {
|
|
if first.MissingInStorage[i].StorageKey != w {
|
|
t.Fatalf("sortierreihenfolge falsch: %v, want beginnend mit %v", first.MissingInStorage, wantOrder)
|
|
}
|
|
}
|
|
}
|