QA-01: Teststrategie Archive (Pflichttest-Gate, Referenzdatensatz, CI-Workflow)
Archive-Pflichttest-Gate / pflichttest-gate (pull_request) Failing after 31s
Archive-Pflichttest-Gate / pflichttest-gate (pull_request) Failing after 31s
Gespiegelt von Core QA-01: Testpyramide (Unit/Integration/Wiederherstellungs- test/Extern), Pflichttests fuer sicherheitskritischen Code als technisch erzwungenes Merge-Gate statt Dokumentationsvorgabe. Sicherheitskritische Bereiche fuer Archive (docs/TESTSTRATEGIE-ARCHIVE.md Abschnitt 3): wormstore (WORM-Durchsetzung selbst), rbacclient/retentionapi (Zugriffspruefung), tenantregistryclient/kekclient/backupcrypto (Tenant-/ Schluessel-Bezug). internal/pflichttestgate + cmd/pflichttestgate pruefen das automatisiert gegen den PR-Diff, .gitea/workflows/archive-pflichttest- gate.yml fuehrt es in CI aus (Pfad-Filter auf archive/**, da Monorepo mit mehreren physisch getrennten Go-Modulen). internal/referenceset liefert den in Abschnitt 4 definierten, synthetischen Referenzdatensatz (Retention-Objekte je Fristklasse, WORM-Nutzdaten in drei Groessenklassen, Backup-Generationen mit geteilten/eindeutigen Bloecken) - keine echten Kunden-/Mandantendaten (DSGVO-Vorgabe), reproduzierbar per festem Seed, Nachweis in referenceset_test.go. Offen: Gegenlesen durch zweite Person (Akzeptanzkriterium 3) - wie bei Core QA-01 im PR nachzutragen, blockiert nicht das technische Inkrafttreten des Gates. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
a5c0bd6c89
commit
81dca7779e
7 files changed
+464
No files matched your search
@@ -0,0 +1,23 @@
|
||||
name: Archive-Pflichttest-Gate
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- "archive/**"
|
||||
|
||||
jobs:
|
||||
pflichttest-gate:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: "1.22"
|
||||
- name: Gate bauen
|
||||
working-directory: archive
|
||||
run: go build -o /tmp/pflichttestgate ./cmd/pflichttestgate
|
||||
- name: Geänderte Dateien gegen Pflichttest-Regel pruefen
|
||||
run: |
|
||||
git diff --name-only "origin/${{ github.base_ref }}...HEAD" -- archive/ | /tmp/pflichttestgate
|
||||
@@ -0,0 +1,41 @@
|
||||
// Command pflichttestgate ist das CI-Gate aus docs/TESTSTRATEGIE-ARCHIVE.md Abschnitt 5.
|
||||
// Aufruf: pflichttestgate <geänderte-Dateien-getrennt-durch-Zeilenumbruch-via-stdin>
|
||||
// Beendet sich mit Exit-Code 1 und einer Liste der Verstöße, wenn ein sicherheitskritisches
|
||||
// Package ohne begleitende Testdatei geändert wurde.
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"gitea.perlbach24.de/scripte/nexarch/archive/internal/pflichttestgate"
|
||||
)
|
||||
|
||||
func main() {
|
||||
var changedFiles []string
|
||||
scanner := bufio.NewScanner(os.Stdin)
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
if line != "" {
|
||||
changedFiles = append(changedFiles, line)
|
||||
}
|
||||
}
|
||||
if err := scanner.Err(); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "pflichttestgate: Eingabe konnte nicht gelesen werden: %v\n", err)
|
||||
os.Exit(2)
|
||||
}
|
||||
|
||||
violations := pflichttestgate.CheckDiff(changedFiles)
|
||||
if len(violations) == 0 {
|
||||
fmt.Println("pflichttestgate: bestanden — alle sicherheitskritischen Änderungen haben begleitende Tests.")
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Fprintln(os.Stderr, "pflichttestgate: FEHLGESCHLAGEN — Pflichttest fehlt für:")
|
||||
for _, v := range violations {
|
||||
fmt.Fprintf(os.Stderr, " - Package %q (Datei %q hat keine begleitende _test.go-Änderung)\n", v.Package, v.ChangedFile)
|
||||
}
|
||||
fmt.Fprintln(os.Stderr, "\nSiehe docs/TESTSTRATEGIE-ARCHIVE.md Abschnitt 3.")
|
||||
os.Exit(1)
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
# NEXARCH Archive – Teststrategie
|
||||
|
||||
Stand: 2026-09-17. Ticket: QA-01.
|
||||
|
||||
## 1. Warum dieses Dokument existiert
|
||||
|
||||
Core übernimmt aus archivdms/archivmail den Grundsatz, dass Testpflicht für sicherheitskritischen
|
||||
Code ein Merge-Gate ist, keine Nachrüstung (siehe `docs/TESTSTRATEGIE-CORE.md`, Core QA-01). Archive
|
||||
trägt zusätzliches, eigenes Risiko: Es ist die letzte Verteidigungslinie gegen Datenverlust
|
||||
(Backup/Restore) und die technische Durchsetzung gesetzlicher Aufbewahrungspflichten (WORM,
|
||||
Löschsperren). Ein ungetesteter Regressionsfehler hier bedeutet im schlechtesten Fall ein
|
||||
unwiederherstellbares oder ein vorzeitig gelöschtes, aufbewahrungspflichtiges Dokument.
|
||||
|
||||
## 2. Testpyramide
|
||||
|
||||
| Ebene | Werkzeug | Umfang | Verantwortlich |
|
||||
|---|---|---|---|
|
||||
| Unit | `go test` (Standardbibliothek) | Einzelne Funktionen/Typen, keine externe Abhängigkeit (DB, Dateisystem, Netzwerk) | Autor der Änderung |
|
||||
| Integration | `go test` gegen echte PostgreSQL-Instanz (`nexarch_test`-Rolle) + echtes Dateisystem für WORM-Locks | Retention-Engine, WORM-Speicher, Backup/Restore-Zyklen, Client-Pakete gegen Core (RBAC, KEK, Tenant-Registry) | Autor der Änderung |
|
||||
| Wiederherstellungstest | `go test` mit echtem `restic`/`pg_combinebackup` gegen den Testhost | Vollständiger Backup→Restore-Zyklus inkl. Envelope-Verschlüsselung (BAK-01 bis BAK-09) | Autor der Änderung, Abnahme durch QA-05 |
|
||||
| Extern | Archive `QA-05`/`QA-07` (Abnahme-/Vertragstests) | Modulübergreifende Verträge (Retention-Adapter, Audit-Ingest) | Siehe jeweiliges Ticket |
|
||||
|
||||
Diese Kachel (QA-01) legt die Pflichtebenen fest und erzwingt sie technisch für die
|
||||
sicherheitskritischsten Bereiche; sie ersetzt nicht QA-02 bis QA-06, die eigene Prüfgates haben.
|
||||
|
||||
## 3. Pflichttests als Merge-Gate
|
||||
|
||||
Verbindlich für jeden Pull Request, der Dateien in einem der folgenden Bereiche ändert:
|
||||
|
||||
- **WORM-Durchsetzung** (`archive/internal/wormstore/`) — die Storage-Object-Lock-Garantie selbst.
|
||||
- **Zugriffsprüfung** (`archive/internal/rbacclient/`, `archive/internal/retentionapi/`) — RBAC-Anbindung
|
||||
und die APIs, die sie durchsetzen.
|
||||
- **Tenant-/Schlüssel-Bezug** (`archive/internal/tenantregistryclient/`, `archive/internal/kekclient/`,
|
||||
`archive/internal/backupcrypto/`) — unabhängige Tenant-Validierung und Verschlüsselungspfade.
|
||||
|
||||
Regel: **jede geänderte `.go`-Datei in einem dieser Bereiche muss von einer geänderten oder neuen
|
||||
`_test.go`-Datei im selben Package begleitet sein.** Das CI-Gate (Abschnitt 5) prüft das
|
||||
automatisiert und blockiert den Merge, wenn die Regel verletzt ist.
|
||||
|
||||
Diese Regel gilt projektweit für alle sieben Boards, nicht nur Core — siehe `SICHERHEITSKONZEPT.md`
|
||||
Abschnitt 11, Punkt 8, sowie die jeweiligen `QA-01`-Tickets der anderen Boards.
|
||||
|
||||
## 4. Referenzdatensatz für Retention-/Backup-Tests
|
||||
|
||||
Ein reproduzierbarer, synthetischer Referenzdatensatz (keine echten Kunden-/Mandantendaten,
|
||||
siehe `CLAUDE.md` DSGVO-Vorgabe) für Retention- und Backup-Integrationstests:
|
||||
|
||||
- **Retention-Objekte**: drei synthetische Objekttypen (`dms_document`, `mail_message`,
|
||||
`audit_event`) mit je einer Aufbewahrungsklasse (`steuerlich_10j`, `handelsrechtlich_6j`,
|
||||
`sofort_loeschbar`) — deckt kurze, mittlere und lange Fristen sowie Sofort-Ablauf ab.
|
||||
- **WORM-Nutzdaten**: kleine, deterministisch erzeugte Dateien (`crypto/rand`-Seed fest verdrahtet
|
||||
im Testcode, keine externen Fixtures) in drei Größenklassen (1 KiB, 1 MiB, 10 MiB) — deckt
|
||||
Prüfsummenbildung und Object-Lock unabhängig von der Dateigröße ab.
|
||||
- **Backup-Generationen**: eine Voll- und zwei Inkrement-Generationen mit überlappenden und
|
||||
eindeutigen Blöcken — deckt Deduplizierung (BAK-01/02) und Envelope-Verschlüsselung (BAK-09) ab.
|
||||
- **Erzeugung**: jeder Testfall erzeugt seinen Anteil des Referenzdatensatzes selbst zur Laufzeit
|
||||
(`t.TempDir()`, deterministische Testdaten-Generatoren in den jeweiligen `_test.go`-Dateien) und
|
||||
räumt ihn über `t.Cleanup()` wieder ab — keine geteilten Fixture-Dateien zwischen Testfällen, aus
|
||||
demselben Grund wie Core Abschnitt 4 (Isolation zwischen parallelen Testläufen auf demselben
|
||||
physischen Testhost).
|
||||
- **Nachweis der Reproduzierbarkeit**: `internal/referenceset/referenceset_test.go` (dieses Ticket)
|
||||
baut den vollständigen Referenzdatensatz zweimal unabhängig voneinander auf und vergleicht die
|
||||
Ergebnisse (Prüfsummen, Objekt-/Klassenzuordnungen) — Prüfung 1 dieses Tickets.
|
||||
|
||||
## 5. CI-Gate
|
||||
|
||||
`.gitea/workflows/archive-pflichttest-gate.yml` führt `archive/cmd/pflichttestgate` gegen den
|
||||
PR-Diff aus (`git diff --name-only origin/<base>...HEAD -- archive/`). Das Programm:
|
||||
|
||||
1. Filtert die geänderten Dateien auf die in Abschnitt 3 genannten Pfad-Muster.
|
||||
2. Prüft je betroffenem Go-Package, ob mindestens eine `_test.go`-Datei desselben Packages ebenfalls
|
||||
im Diff enthalten ist.
|
||||
3. Beendet sich mit Exit-Code 1 und einer Liste der betroffenen Packages ohne Teständerung, wenn die
|
||||
Regel verletzt ist — der CI-Job schlägt dann fehl, der Merge ist blockiert.
|
||||
|
||||
Negativtest des Gates selbst: `internal/pflichttestgate/gate_test.go` enthält einen Testfall, der
|
||||
einen Diff mit geänderter `archive/internal/wormstore/wormstore.go` ohne begleitende Testdatei
|
||||
simuliert und erwartet, dass das Gate das als Verstoß erkennt (Prüfung 1 dieses Tickets — gespiegelt
|
||||
zur Referenzdatensatz-Prüfung in Abschnitt 4, beide sind Teil derselben Abnahme).
|
||||
|
||||
## 6. Dokumentation & Gegenlesen
|
||||
|
||||
Dieses Dokument ist von einer zweiten Person gegenzulesen, bevor die Kachel als abgeschlossen gilt
|
||||
(Akzeptanzkriterium 3 / Prüfung 2). **Offen zum Zeitpunkt dieses Commits** — wie bei Core QA-01 wird
|
||||
das Gegenlesen im Pull Request nachgetragen, sobald eine zweite Person verfügbar ist; es blockiert
|
||||
nicht das technische Inkrafttreten des Gates (Abschnitt 5 ist bereits automatisiert scharf).
|
||||
@@ -0,0 +1,70 @@
|
||||
// Package pflichttestgate erzwingt die in docs/TESTSTRATEGIE-ARCHIVE.md Abschnitt 3 festgelegte
|
||||
// Regel: jede geänderte Go-Datei in einem sicherheitskritischen Bereich (WORM-Durchsetzung,
|
||||
// Zugriffsprüfung, Tenant-/Schlüssel-Bezug) muss von einer geänderten oder neuen _test.go-Datei
|
||||
// im selben Package begleitet sein. Selbe Logik wie Core QA-01 (internal/pflichttestgate),
|
||||
// hier für Archive gespiegelt statt gemeinsam genutzt (physisch getrenntes Go-Modul).
|
||||
package pflichttestgate
|
||||
|
||||
import (
|
||||
"path"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// sensitivePathPatterns beschreibt die Bereiche aus TESTSTRATEGIE-ARCHIVE.md Abschnitt 3.
|
||||
var sensitivePathPatterns = []*regexp.Regexp{
|
||||
regexp.MustCompile(`(^|/)archive/internal/wormstore/`),
|
||||
regexp.MustCompile(`(^|/)archive/internal/rbacclient/`),
|
||||
regexp.MustCompile(`(^|/)archive/internal/tenantregistryclient/`),
|
||||
regexp.MustCompile(`(^|/)archive/internal/retentionapi/`),
|
||||
regexp.MustCompile(`(^|/)archive/internal/kekclient/`),
|
||||
regexp.MustCompile(`(^|/)archive/internal/backupcrypto/`),
|
||||
}
|
||||
|
||||
// Violation beschreibt ein Package mit sicherheitskritischer Änderung ohne begleitende Testdatei.
|
||||
type Violation struct {
|
||||
Package string
|
||||
ChangedFile string
|
||||
}
|
||||
|
||||
// isSensitive meldet, ob eine geänderte Datei in einen der Pflichttest-Bereiche fällt.
|
||||
func isSensitive(file string) bool {
|
||||
if !strings.HasSuffix(file, ".go") || strings.HasSuffix(file, "_test.go") {
|
||||
return false
|
||||
}
|
||||
for _, re := range sensitivePathPatterns {
|
||||
if re.MatchString(file) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// CheckDiff prüft eine Liste geänderter Dateipfade (z. B. aus `git diff --name-only`) gegen die
|
||||
// Pflichttest-Regel und gibt für jedes betroffene Package ohne begleitende Testdatei eine Violation
|
||||
// zurück. Ein leeres Ergebnis bedeutet: Gate besteht.
|
||||
func CheckDiff(changedFiles []string) []Violation {
|
||||
sensitiveByPkg := map[string]string{}
|
||||
testTouchedPkgs := map[string]bool{}
|
||||
|
||||
for _, f := range changedFiles {
|
||||
pkg := path.Dir(f)
|
||||
if strings.HasSuffix(f, "_test.go") {
|
||||
testTouchedPkgs[pkg] = true
|
||||
continue
|
||||
}
|
||||
if isSensitive(f) {
|
||||
if _, seen := sensitiveByPkg[pkg]; !seen {
|
||||
sensitiveByPkg[pkg] = f
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var violations []Violation
|
||||
for pkg, file := range sensitiveByPkg {
|
||||
if !testTouchedPkgs[pkg] {
|
||||
violations = append(violations, Violation{Package: pkg, ChangedFile: file})
|
||||
}
|
||||
}
|
||||
return violations
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
package pflichttestgate
|
||||
|
||||
import "testing"
|
||||
|
||||
// Prüfung 1 (QA-01): Diff mit absichtlich fehlendem Pflichttest wird als Verstoß erkannt —
|
||||
// Negativtest des Gates selbst.
|
||||
func TestCheckDiff_BlocksWormstoreChangeWithoutTest(t *testing.T) {
|
||||
changed := []string{
|
||||
"archive/internal/wormstore/wormstore.go",
|
||||
"archive/internal/backup/manifest.go", // unkritisch, keine Testpflicht
|
||||
}
|
||||
|
||||
violations := CheckDiff(changed)
|
||||
|
||||
if len(violations) != 1 {
|
||||
t.Fatalf("erwartet 1 Verstoß, bekommen %d: %+v", len(violations), violations)
|
||||
}
|
||||
if violations[0].Package != "archive/internal/wormstore" {
|
||||
t.Errorf("erwartetes Package archive/internal/wormstore, bekommen %q", violations[0].Package)
|
||||
}
|
||||
if violations[0].ChangedFile != "archive/internal/wormstore/wormstore.go" {
|
||||
t.Errorf("erwartete Datei archive/internal/wormstore/wormstore.go, bekommen %q", violations[0].ChangedFile)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckDiff_PassesWhenTestFileAccompaniesChange(t *testing.T) {
|
||||
changed := []string{
|
||||
"archive/internal/wormstore/wormstore.go",
|
||||
"archive/internal/wormstore/wormstore_test.go",
|
||||
}
|
||||
|
||||
violations := CheckDiff(changed)
|
||||
|
||||
if len(violations) != 0 {
|
||||
t.Fatalf("erwartet keinen Verstoß, bekommen %+v", violations)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckDiff_PassesForNewTestFileEvenWithoutSourceChange(t *testing.T) {
|
||||
changed := []string{"archive/internal/rbacclient/rbacclient_test.go"}
|
||||
|
||||
violations := CheckDiff(changed)
|
||||
|
||||
if len(violations) != 0 {
|
||||
t.Fatalf("erwartet keinen Verstoß, bekommen %+v", violations)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckDiff_IgnoresUnrelatedPackages(t *testing.T) {
|
||||
changed := []string{"archive/internal/retentionengine/retentionengine.go", "archive/internal/reconcile/reconcile.go"}
|
||||
|
||||
violations := CheckDiff(changed)
|
||||
|
||||
if len(violations) != 0 {
|
||||
t.Fatalf("erwartet keinen Verstoß fuer unkritische Pakete, bekommen %+v", violations)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckDiff_CoversAllSensitiveAreas(t *testing.T) {
|
||||
cases := []string{
|
||||
"archive/internal/wormstore/wormstore.go",
|
||||
"archive/internal/rbacclient/rbacclient.go",
|
||||
"archive/internal/tenantregistryclient/tenantregistryclient.go",
|
||||
"archive/internal/retentionapi/handler.go",
|
||||
"archive/internal/kekclient/kekclient.go",
|
||||
"archive/internal/backupcrypto/reposecret.go",
|
||||
}
|
||||
for _, f := range cases {
|
||||
violations := CheckDiff([]string{f})
|
||||
if len(violations) != 1 {
|
||||
t.Errorf("Datei %q sollte als sensibel erkannt werden, Verstöße: %+v", f, violations)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckDiff_IgnoresNonGoFiles(t *testing.T) {
|
||||
changed := []string{"archive/internal/wormstore/README.md"}
|
||||
|
||||
violations := CheckDiff(changed)
|
||||
|
||||
if len(violations) != 0 {
|
||||
t.Fatalf("Nicht-Go-Dateien duerfen keine Testpflicht ausloesen, bekommen %+v", violations)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
// Package referenceset implementiert den in docs/TESTSTRATEGIE-ARCHIVE.md Abschnitt 4 definierten
|
||||
// Referenzdatensatz für Retention- und Backup-Integrationstests: synthetische Objekttypen,
|
||||
// deterministisch erzeugte WORM-Nutzdaten in drei Größenklassen und Backup-Generationen mit
|
||||
// überlappenden/eindeutigen Blöcken — reproduzierbar, ohne echte Kunden-/Mandantendaten.
|
||||
package referenceset
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"math/rand"
|
||||
)
|
||||
|
||||
// RetentionObject ist ein synthetisches, retention-pflichtiges Objekt des Referenzdatensatzes.
|
||||
type RetentionObject struct {
|
||||
ObjectType string
|
||||
ObjectReference string
|
||||
RetentionClass string
|
||||
}
|
||||
|
||||
// RetentionObjects liefert die drei Objekttypen mit je einer Aufbewahrungsklasse aus Abschnitt 4:
|
||||
// kurze, mittlere und lange Frist sowie Sofort-Ablauf.
|
||||
func RetentionObjects() []RetentionObject {
|
||||
return []RetentionObject{
|
||||
{ObjectType: "dms_document", ObjectReference: "ref-dms-referenceset-1", RetentionClass: "steuerlich_10j"},
|
||||
{ObjectType: "mail_message", ObjectReference: "ref-mail-referenceset-1", RetentionClass: "handelsrechtlich_6j"},
|
||||
{ObjectType: "audit_event", ObjectReference: "ref-audit-referenceset-1", RetentionClass: "sofort_loeschbar"},
|
||||
}
|
||||
}
|
||||
|
||||
// WormPayloadSize benennt eine der drei Größenklassen aus Abschnitt 4.
|
||||
type WormPayloadSize int
|
||||
|
||||
const (
|
||||
SizeSmall WormPayloadSize = 1 * 1024 // 1 KiB
|
||||
SizeMedium WormPayloadSize = 1 * 1024 * 1024 // 1 MiB
|
||||
SizeLarge WormPayloadSize = 10 * 1024 * 1024 // 10 MiB
|
||||
)
|
||||
|
||||
// wormSeed ist fest verdrahtet, damit WormPayload für dieselbe Größenklasse bei jedem Aufruf
|
||||
// exakt denselben Inhalt (und damit dieselbe Prüfsumme) liefert — keine externen Fixture-Dateien,
|
||||
// aber trotzdem reproduzierbar (Prüfung 1: zwei unabhängige Aufbauten liefern identische Prüfsummen).
|
||||
const wormSeed = 20260917
|
||||
|
||||
// WormPayload erzeugt deterministische Nutzdaten der gegebenen Größenklasse.
|
||||
func WormPayload(size WormPayloadSize) []byte {
|
||||
buf := make([]byte, int(size))
|
||||
rnd := rand.New(rand.NewSource(wormSeed + int64(size)))
|
||||
_, _ = rnd.Read(buf)
|
||||
return buf
|
||||
}
|
||||
|
||||
// WormPayloadChecksum liefert die SHA-256-Prüfsumme der deterministischen Nutzdaten einer
|
||||
// Größenklasse — Vergleichswert für den Reproduzierbarkeitsnachweis.
|
||||
func WormPayloadChecksum(size WormPayloadSize) string {
|
||||
sum := sha256.Sum256(WormPayload(size))
|
||||
return hex.EncodeToString(sum[:])
|
||||
}
|
||||
|
||||
// BackupBlock ist EIN Block einer synthetischen Backup-Generation.
|
||||
type BackupBlock struct {
|
||||
Generation int // 0 = Vollsicherung, 1/2 = Inkremente
|
||||
Key string
|
||||
Content []byte
|
||||
}
|
||||
|
||||
// BackupGenerations liefert eine Voll- und zwei Inkrement-Generationen mit überlappenden
|
||||
// (unverändertes Blockduplikat über Generationen hinweg) und eindeutigen Blöcken — Grundlage
|
||||
// für Deduplizierungs- (BAK-01/02) und Envelope-Verschlüsselungstests (BAK-09).
|
||||
func BackupGenerations() []BackupBlock {
|
||||
unchanged := WormPayload(SizeSmall) // taucht identisch in allen drei Generationen auf
|
||||
return []BackupBlock{
|
||||
{Generation: 0, Key: "block-unchanged", Content: unchanged},
|
||||
{Generation: 0, Key: "block-full-only", Content: WormPayload(SizeMedium)},
|
||||
{Generation: 1, Key: "block-unchanged", Content: unchanged},
|
||||
{Generation: 1, Key: "block-incr1-only", Content: deriveVariant(unchanged, 1)},
|
||||
{Generation: 2, Key: "block-unchanged", Content: unchanged},
|
||||
{Generation: 2, Key: "block-incr2-only", Content: deriveVariant(unchanged, 2)},
|
||||
}
|
||||
}
|
||||
|
||||
func deriveVariant(base []byte, salt byte) []byte {
|
||||
out := make([]byte, len(base))
|
||||
copy(out, base)
|
||||
if len(out) > 0 {
|
||||
out[0] ^= salt
|
||||
}
|
||||
return out
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
package referenceset
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// Prüfung 1 (QA-01): der Referenzdatensatz lässt sich reproduzierbar aufsetzen — zwei
|
||||
// unabhängige Aufbauten liefern identische Retention-Objekte, Prüfsummen und Backup-Blöcke.
|
||||
func TestReferenceSet_IsReproducibleAcrossIndependentBuilds(t *testing.T) {
|
||||
if !reflect.DeepEqual(RetentionObjects(), RetentionObjects()) {
|
||||
t.Fatal("RetentionObjects() liefert bei zwei Aufrufen unterschiedliche Ergebnisse")
|
||||
}
|
||||
|
||||
for _, size := range []WormPayloadSize{SizeSmall, SizeMedium, SizeLarge} {
|
||||
sum1 := WormPayloadChecksum(size)
|
||||
sum2 := WormPayloadChecksum(size)
|
||||
if sum1 != sum2 {
|
||||
t.Fatalf("groessenklasse %d: pruefsumme nicht reproduzierbar (%q != %q)", size, sum1, sum2)
|
||||
}
|
||||
if len(WormPayload(size)) != int(size) {
|
||||
t.Fatalf("groessenklasse %d: erwartete laenge %d, hatte %d", size, size, len(WormPayload(size)))
|
||||
}
|
||||
}
|
||||
|
||||
blocks1 := BackupGenerations()
|
||||
blocks2 := BackupGenerations()
|
||||
if !reflect.DeepEqual(blocks1, blocks2) {
|
||||
t.Fatal("BackupGenerations() liefert bei zwei Aufrufen unterschiedliche Ergebnisse")
|
||||
}
|
||||
}
|
||||
|
||||
// Deckt die drei Aufbewahrungsfristen-Kategorien aus Abschnitt 4 ab (kurz/mittel/lang/sofort).
|
||||
func TestRetentionObjects_CoversAllRetentionClasses(t *testing.T) {
|
||||
classes := map[string]bool{}
|
||||
for _, o := range RetentionObjects() {
|
||||
classes[o.RetentionClass] = true
|
||||
}
|
||||
for _, want := range []string{"steuerlich_10j", "handelsrechtlich_6j", "sofort_loeschbar"} {
|
||||
if !classes[want] {
|
||||
t.Errorf("erwartete aufbewahrungsklasse %q fehlt im referenzdatensatz", want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Deckt Deduplizierung (identischer Block über Generationen hinweg) UND eindeutige,
|
||||
// generationsspezifische Blöcke ab — Grundlage für BAK-01/02-Tests.
|
||||
func TestBackupGenerations_HasBothSharedAndUniqueBlocks(t *testing.T) {
|
||||
blocks := BackupGenerations()
|
||||
|
||||
byKeyGen := map[string]map[int][]byte{}
|
||||
for _, b := range blocks {
|
||||
if byKeyGen[b.Key] == nil {
|
||||
byKeyGen[b.Key] = map[int][]byte{}
|
||||
}
|
||||
byKeyGen[b.Key][b.Generation] = b.Content
|
||||
}
|
||||
|
||||
shared, ok := byKeyGen["block-unchanged"]
|
||||
if !ok || len(shared) != 3 {
|
||||
t.Fatalf("erwartet block-unchanged in allen 3 generationen, hatte %d", len(shared))
|
||||
}
|
||||
if !reflect.DeepEqual(shared[0], shared[1]) || !reflect.DeepEqual(shared[1], shared[2]) {
|
||||
t.Fatal("block-unchanged sollte in allen generationen identischen inhalt haben")
|
||||
}
|
||||
|
||||
for _, key := range []string{"block-full-only", "block-incr1-only", "block-incr2-only"} {
|
||||
if _, ok := byKeyGen[key]; !ok {
|
||||
t.Errorf("erwarteter eindeutiger block %q fehlt", key)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user