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_ExistingInStorageIsStableSamplingBasis ist der Nachweis, // dass Reconcile eine deterministisch sortierte Liste ALLER bestaetigt // existierenden Objekte liefert (DB-Eintrag UND Storage-Objekt vorhanden) // - dies ist die Stichprobengrundlage, die Archive BAK-08 weiterverwendet, // ohne selbst neu zu sortieren/filtern. func TestReconcile_ExistingInStorageIsStableSamplingBasis(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/fehlt/revisions/r1", DocumentID: "fehlt", RevisionID: "r1"}, } storage := []string{ "documents/z/revisions/r1", "documents/a/revisions/r1", } report := Reconcile(db, storage) want := []string{"documents/a/revisions/r1", "documents/z/revisions/r1"} if len(report.ExistingInStorage) != len(want) { t.Fatalf("erwartet %d bestaetigt existierende objekte, habe %d: %+v", len(want), len(report.ExistingInStorage), report.ExistingInStorage) } for i, w := range want { if report.ExistingInStorage[i].StorageKey != w { t.Fatalf("sortierreihenfolge falsch: %v, want beginnend mit %v", report.ExistingInStorage, want) } } if len(report.MissingInStorage) != 1 || report.MissingInStorage[0].StorageKey != "documents/fehlt/revisions/r1" { t.Fatalf("missing_in_storage unerwartet: %+v", report.MissingInStorage) } } // 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) } } }