diff --git a/dms/cmd/gen-test-corpus/main.go b/dms/cmd/gen-test-corpus/main.go
new file mode 100644
index 00000000..05dbeb3c
--- /dev/null
+++ b/dms/cmd/gen-test-corpus/main.go
@@ -0,0 +1,33 @@
+// gen-test-corpus erzeugt den QA-01-Referenzkorpus reproduzierbar unter
+// dms/testdata/corpus. Aufruf: go run ./cmd/gen-test-corpus
+package main
+
+import (
+ "fmt"
+ "os"
+ "path/filepath"
+
+ "gitea.perlbach24.de/scripte/nexarch/dms/internal/testcorpus"
+)
+
+func main() {
+ files, err := testcorpus.Generate()
+ if err != nil {
+ fmt.Fprintln(os.Stderr, "Korpus-Erzeugung fehlgeschlagen:", err)
+ os.Exit(1)
+ }
+
+ root := "testdata/corpus"
+ for _, f := range files {
+ dst := filepath.Join(root, f.Path)
+ if err := os.MkdirAll(filepath.Dir(dst), 0o755); err != nil {
+ fmt.Fprintln(os.Stderr, "Verzeichnis anlegen fehlgeschlagen:", err)
+ os.Exit(1)
+ }
+ if err := os.WriteFile(dst, f.Content, 0o644); err != nil {
+ fmt.Fprintln(os.Stderr, "Datei schreiben fehlgeschlagen:", err)
+ os.Exit(1)
+ }
+ fmt.Println("erzeugt:", dst)
+ }
+}
diff --git a/dms/docs/QA-01-TESTSTRATEGIE.md b/dms/docs/QA-01-TESTSTRATEGIE.md
new file mode 100644
index 00000000..e05a6df0
--- /dev/null
+++ b/dms/docs/QA-01-TESTSTRATEGIE.md
@@ -0,0 +1,92 @@
+# QA-01 – Teststrategie DMS-Modul
+
+Welle 3. Baut auf `FDN-07` (CI-Pipeline & Testharness) auf und formalisiert
+deren Merge-Gate-Mechanik zu einer schriftlichen Strategie, ohne sie neu zu
+implementieren.
+
+## Testpyramide
+
+Stack: Go-Backend (`dms/`, Module pro fachlichem Bereich unter `internal/`)
++ Next.js-Frontends (`web/shl`, `web/dms-browser`, `web/pdf-viewer`).
+
+| Ebene | Anteil (Zielbild) | Was hier getestet wird | Werkzeug |
+|---|---|---|---|
+| Unit | ca. 70% | Einzelne Funktionen/Methoden innerhalb eines `internal/`-Pakets: Validierung, Zustandsübergänge, Fehlerpfade, reine Berechnungen (z. B. Checksummen, Diff-Logik in `internal/versioning`). Keine echte DB/kein echtes S3 nötig, wo Interfaces das zulassen. | `go test` |
+| Integration | ca. 25% | Zusammenspiel mehrerer Pakete gegen eine echte, pro Lauf frisch aufgesetzte Testdatenbank (`scripts/reset-test-env.sh`, FDN-01/FDN-07-Konvention) bzw. echten Storage-Treiber: HTTP-Handler-Tests (`*api`-Pakete) mit echter Transaktion, Upload-Pfad Ende-zu-Ende innerhalb des Go-Prozesses. | `go test` gegen `NEXARCH_DMS_TEST_DB_PASSWORD`-DB |
+| E2E | ca. 5% | Kompletter Durchlauf durch die Next.js-Oberflächen gegen ein laufendes Backend (Browser-Interaktion). Bewusst klein gehalten: teuer, langsam, wartungsintensiv – nur für die kritischsten Nutzerpfade (Login/Upload/Dokumentenansicht), sobald diese Kacheln existieren. | Vitest/Playwright-Äquivalent im `web/*`-Bereich, aktuell nur Komponenten-/Typtests (`vitest run --coverage`, `tsc --noEmit` laut `ci.yml`) |
+
+Begründung für die Gewichtung: Business-Logik (Tenant-Scoping,
+Verschlüsselung, Zustandsmaschinen bei Versionierung/Retention) liegt fast
+vollständig im Go-Backend; die Frontends sind überwiegend Darstellung
+bestehender API-Daten. Ein hoher E2E-Anteil würde hier vor allem Laufzeit
+kosten, ohne proportional mehr Fehler zu finden.
+
+## Abdeckungsziele
+
+- **Sicherheitsrelevante Pakete** (siehe unten): Pflicht, keine Zahl als
+ Ziel, sondern *jede* neue Funktion mit Tenant-Bezug/Zugriffsprüfung/
+ kryptografischer Operation braucht einen Test, bevor sie gemerged wird
+ (siehe Merge-Gate unten). Das ist die schärfere, verbindliche Regel –
+ eine reine Prozentzahl (z. B. "80% Coverage") lässt sich erfüllen, ohne
+ den kritischen Pfad zu treffen.
+- **Übrige `internal/`-Pakete**: Richtwert 60% Zeilenabdeckung
+ (`go tool cover -func`, bereits in `ci.yml` ausgewiesen), als Frühwarnsystem
+ für unbeabsichtigt untestierten Code – kein hartes Gate, weil pauschale
+ Prozentziele in Nicht-Kernpaketen leicht zu Alibi-Tests statt echter
+ Fehlervermeidung führen.
+- **Frontend**: `tsc --noEmit` (Typsicherheit) + `vitest run --coverage`
+ als Basis, kein Zahlenziel in dieser Kachel festgelegt – gehört in die
+ jeweiligen Frontend-Kacheln.
+
+## Pflichttests für sicherheitsrelevanten Code (verbindliches Merge-Gate)
+
+Bekannter Fehler aus Vorgängerprojekten: Auth, Tenant-Scoping und
+Protokoll-Zustandsmaschinen blieben trotz hoher Kritikalität oft ungetestet.
+Deshalb gilt ab dieser Kachel verbindlich, nicht optional:
+
+> Kein Merge von Code in einem als sicherheitsrelevant eingestuften Paket
+> ohne mindestens eine begleitende Testdatei mit mindestens einem
+> `func Test...`.
+
+Technisch durchgesetzt über das bereits in FDN-07 eingeführte
+`dms/scripts/check-security-test-coverage.sh`, als `dms-security-gate`-Job
+in `.gitea/workflows/ci.yml` verankert und `dms-build-test` per `needs:`
+nachgeschaltet (kein grüner Gesamtbuild ohne bestandenes Gate). Diese Kachel
+erweitert die Paketliste um drei real per Tenant-Bezug (`TenantID`)
+identifizierte Pakete, die in FDN-07 noch fehlten:
+
+```
+internal/crypto (bereits FDN-07)
+internal/storage (bereits FDN-07)
+internal/downloadapi (bereits FDN-07)
+internal/versioningapi (bereits FDN-07)
+internal/audittrail (bereits FDN-07)
+internal/documentapi (neu, QA-01: Tenant-Bezug in handler.go)
+internal/upload (neu, QA-01: Tenant-Bezug in service.go)
+internal/versioning (neu, QA-01: Tenant-Bezug in versioning.go/diff.go)
+```
+
+Kein eigenständiges Auth-Paket existiert im Repo noch nicht (Platzhalter,
+Liste bei dessen Einführung ergänzen – unverändert aus FDN-07 übernommen).
+
+Diese Kachel implementiert das Gate nicht neu, sondern erweitert nur dessen
+Datenliste (kleinste Lösung, kein Umbau des Mechanismus).
+
+## Referenzkorpus
+
+Siehe `dms/testdata/corpus/` (erzeugt durch `go run ./cmd/gen-test-corpus`
+aus `dms/internal/testcorpus`). Formate: PDF (Rechnung, Anschreiben), Office
+(.docx, .xlsx), Scan-Ersatz (.tiff, da Scans im System laut DOC-19/INT-06
+als TIFF verarbeitet werden). Details und DSGVO-Erklärung siehe
+`dms/docs/QA-01-PRUEFPROTOKOLL.md`.
+
+## Nicht Bestandteil dieser Kachel
+
+Konkrete Prüfgates für einzelne fachliche Bereiche (Dokumentenkern, Suche/
+Texterkennung, Metadaten/Klassifizierung, Workflow/Benachrichtigung,
+Last-/Leistungstest) sind eigene Kacheln (QA-03, QA-04, QA-05, QA-07,
+QA-10). Diese Strategie liefert nur den Rahmen (Pyramide, Abdeckungsziele,
+Security-Gate) und den Referenzkorpus, auf dem jene Kacheln später ihre
+eigenen, fachspezifischen Prüfungen aufbauen können – der Korpus in
+`dms/testdata/corpus/` ist bewusst so angelegt, dass er von künftigen
+Prüfgate-Kacheln direkt als Eingabedaten wiederverwendet werden kann.
diff --git a/dms/internal/testcorpus/corpus.go b/dms/internal/testcorpus/corpus.go
new file mode 100644
index 00000000..cb251149
--- /dev/null
+++ b/dms/internal/testcorpus/corpus.go
@@ -0,0 +1,70 @@
+package testcorpus
+
+// File ist eine erzeugte Korpus-Datei mit ihrem relativen Zielpfad.
+type File struct {
+ Path string
+ Content []byte
+}
+
+// Generate baut den vollstaendigen Referenzkorpus. Alle Inhalte sind frei
+// erfunden (fiktive Firma "Musterhandel Nordlicht GmbH", fiktive Personen) -
+// keine Ableitung aus echten Kunden-/Mandantendaten.
+func Generate() ([]File, error) {
+ var files []File
+
+ invoicePDF := GeneratePDF("Rechnung 2026-0001", []string{
+ "Musterhandel Nordlicht GmbH",
+ "Fjordweg 12, 24939 Fiktivstadt",
+ "",
+ "Rechnung Nr. 2026-0001 vom 03.02.2026",
+ "Kunde: Anna Testperson, Beispielallee 7, 20099 Fiktivhafen",
+ "",
+ "Pos. 1 Bueromaterial-Set 1 Stk. 42,50 EUR",
+ "Pos. 2 Aktenordner A4 10 Stk. 35,00 EUR",
+ "",
+ "Gesamtbetrag: 77,50 EUR",
+ })
+ files = append(files, File{Path: "pdf/rechnung-muster-2026-0001.pdf", Content: invoicePDF})
+
+ letterPDF := GeneratePDF("Schreiben Aktenzeichen FIK-2026-042", []string{
+ "Kanzlei Musterrecht Partnerschaft mbB",
+ "Domplatz 3, 33098 Fiktivburg",
+ "",
+ "Ihr Aktenzeichen: FIK-2026-042",
+ "Sehr geehrter Herr Beispielmann,",
+ "",
+ "hiermit bestaetigen wir den Eingang Ihrer Unterlagen vom 10.01.2026.",
+ "Wir werden uns bis zum 28.02.2026 mit weiteren Informationen melden.",
+ "",
+ "Mit freundlichen Gruessen",
+ "Dr. Petra Fiktivnam",
+ })
+ files = append(files, File{Path: "pdf/schreiben-muster-fik-2026-042.pdf", Content: letterPDF})
+
+ docx, err := GenerateDocx([]string{
+ "Widerrufsbelehrung (Muster)",
+ "Musterhandel Nordlicht GmbH, Fjordweg 12, 24939 Fiktivstadt",
+ "Sie haben das Recht, binnen vierzehn Tagen ohne Angabe von Gruenden diesen Vertrag zu widerrufen.",
+ "Die Widerrufsfrist betraegt vierzehn Tage ab dem Tag des Vertragsabschlusses.",
+ })
+ if err != nil {
+ return nil, err
+ }
+ files = append(files, File{Path: "office/widerrufsbelehrung-muster.docx", Content: docx})
+
+ xlsx, err := GenerateXlsx([][]string{
+ {"Position", "Artikel", "Menge", "Einzelpreis", "Gesamt"},
+ {"1", "Bueromaterial-Set", "1", "42,50", "42,50"},
+ {"2", "Aktenordner A4", "10", "3,50", "35,00"},
+ {"", "", "", "Summe", "77,50"},
+ })
+ if err != nil {
+ return nil, err
+ }
+ files = append(files, File{Path: "office/rechnungspositionen-muster.xlsx", Content: xlsx})
+
+ scan := GenerateTIFF(600, 800, 0xE8)
+ files = append(files, File{Path: "scan/scan-muster-eingangspost-001.tiff", Content: scan})
+
+ return files, nil
+}
diff --git a/dms/internal/testcorpus/corpus_test.go b/dms/internal/testcorpus/corpus_test.go
new file mode 100644
index 00000000..0c2c440f
--- /dev/null
+++ b/dms/internal/testcorpus/corpus_test.go
@@ -0,0 +1,110 @@
+package testcorpus
+
+import (
+ "archive/zip"
+ "bytes"
+ "encoding/binary"
+ "strings"
+ "testing"
+)
+
+func TestGenerate_AllFormatsCoveredAndParseable(t *testing.T) {
+ files, err := Generate()
+ if err != nil {
+ t.Fatalf("Generate: %v", err)
+ }
+
+ wantExt := map[string]bool{".pdf": false, ".docx": false, ".xlsx": false, ".tiff": false}
+ for _, f := range files {
+ for ext := range wantExt {
+ if strings.HasSuffix(f.Path, ext) {
+ wantExt[ext] = true
+ }
+ }
+ }
+ for ext, found := range wantExt {
+ if !found {
+ t.Errorf("kein Korpus-Eintrag mit Endung %s", ext)
+ }
+ }
+
+ for _, f := range files {
+ switch {
+ case strings.HasSuffix(f.Path, ".pdf"):
+ assertValidPDF(t, f.Path, f.Content)
+ case strings.HasSuffix(f.Path, ".docx"), strings.HasSuffix(f.Path, ".xlsx"):
+ assertValidOOXMLZip(t, f.Path, f.Content)
+ case strings.HasSuffix(f.Path, ".tiff"):
+ assertValidTIFF(t, f.Path, f.Content)
+ }
+ }
+}
+
+func assertValidPDF(t *testing.T, path string, content []byte) {
+ t.Helper()
+ if !bytes.HasPrefix(content, []byte("%PDF-")) {
+ t.Errorf("%s: fehlender PDF-Header", path)
+ }
+ if !bytes.Contains(content, []byte("%%EOF")) {
+ t.Errorf("%s: fehlendes PDF-EOF-Marker", path)
+ }
+}
+
+func assertValidOOXMLZip(t *testing.T, path string, content []byte) {
+ t.Helper()
+ r, err := zip.NewReader(bytes.NewReader(content), int64(len(content)))
+ if err != nil {
+ t.Fatalf("%s: kein gueltiges Zip: %v", path, err)
+ }
+ hasContentTypes := false
+ for _, f := range r.File {
+ if f.Name == "[Content_Types].xml" {
+ hasContentTypes = true
+ }
+ }
+ if !hasContentTypes {
+ t.Errorf("%s: [Content_Types].xml fehlt, kein gueltiges OOXML-Paket", path)
+ }
+}
+
+func assertValidTIFF(t *testing.T, path string, content []byte) {
+ t.Helper()
+ if len(content) < 8 || string(content[:2]) != "II" {
+ t.Fatalf("%s: fehlender TIFF-Byteorder-Header", path)
+ }
+ magic := binary.LittleEndian.Uint16(content[2:4])
+ if magic != 42 {
+ t.Errorf("%s: falsches TIFF-Magic %d", path, magic)
+ }
+ ifdOffset := binary.LittleEndian.Uint32(content[4:8])
+ if int(ifdOffset) >= len(content) {
+ t.Fatalf("%s: IFD-Offset ausserhalb der Datei", path)
+ }
+ numTags := binary.LittleEndian.Uint16(content[ifdOffset : ifdOffset+2])
+ if numTags == 0 {
+ t.Errorf("%s: keine IFD-Tags", path)
+ }
+}
+
+func TestGenerateTIFF_DimensionsRoundtrip(t *testing.T) {
+ data := GenerateTIFF(10, 5, 0x80)
+
+ ifdOffset := binary.LittleEndian.Uint32(data[4:8])
+ numTags := binary.LittleEndian.Uint16(data[ifdOffset : ifdOffset+2])
+
+ var width, height uint32
+ for i := 0; i < int(numTags); i++ {
+ entryOff := ifdOffset + 2 + uint32(i*12)
+ tag := binary.LittleEndian.Uint16(data[entryOff : entryOff+2])
+ value := binary.LittleEndian.Uint32(data[entryOff+8 : entryOff+12])
+ switch tag {
+ case 256:
+ width = value
+ case 257:
+ height = value
+ }
+ }
+ if width != 10 || height != 5 {
+ t.Errorf("Breite/Hoehe nicht rekonstruierbar: got %dx%d, want 10x5", width, height)
+ }
+}
diff --git a/dms/internal/testcorpus/office.go b/dms/internal/testcorpus/office.go
new file mode 100644
index 00000000..ab0da179
--- /dev/null
+++ b/dms/internal/testcorpus/office.go
@@ -0,0 +1,142 @@
+package testcorpus
+
+import (
+ "archive/zip"
+ "bytes"
+ "fmt"
+)
+
+const contentTypesDocx = `
+
+
+
+
+`
+
+const rootRelsDocx = `
+
+
+`
+
+const documentXMLTemplate = `
+
+%s
+`
+
+// GenerateDocx baut ein minimal gueltiges .docx (OOXML-Zip) mit den
+// uebergebenen Absaetzen als Flietext.
+func GenerateDocx(paragraphs []string) ([]byte, error) {
+ body := ""
+ for _, p := range paragraphs {
+ body += fmt.Sprintf("%s", escapeXML(p))
+ }
+ documentXML := fmt.Sprintf(documentXMLTemplate, body)
+
+ buf := &bytes.Buffer{}
+ zw := zip.NewWriter(buf)
+ files := map[string]string{
+ "[Content_Types].xml": contentTypesDocx,
+ "_rels/.rels": rootRelsDocx,
+ "word/document.xml": documentXML,
+ }
+ for name, content := range files {
+ w, err := zw.Create(name)
+ if err != nil {
+ return nil, err
+ }
+ if _, err := w.Write([]byte(content)); err != nil {
+ return nil, err
+ }
+ }
+ if err := zw.Close(); err != nil {
+ return nil, err
+ }
+ return buf.Bytes(), nil
+}
+
+const contentTypesXlsx = `
+
+
+
+
+
+`
+
+const rootRelsXlsx = `
+
+
+`
+
+const workbookXML = `
+
+
+`
+
+const workbookRels = `
+
+
+`
+
+// GenerateXlsx baut eine minimal gueltige .xlsx mit den uebergebenen Zeilen
+// als Inline-Strings (kein Shared-String-Table, absichtlich einfach).
+func GenerateXlsx(rows [][]string) ([]byte, error) {
+ sheet := `
+`
+ for r, row := range rows {
+ sheet += fmt.Sprintf(``, r+1)
+ for c, cell := range row {
+ ref := fmt.Sprintf("%s%d", columnLetter(c), r+1)
+ sheet += fmt.Sprintf(`%s`, ref, escapeXML(cell))
+ }
+ sheet += `
`
+ }
+ sheet += ``
+
+ buf := &bytes.Buffer{}
+ zw := zip.NewWriter(buf)
+ files := map[string]string{
+ "[Content_Types].xml": contentTypesXlsx,
+ "_rels/.rels": rootRelsXlsx,
+ "xl/workbook.xml": workbookXML,
+ "xl/_rels/workbook.xml.rels": workbookRels,
+ "xl/worksheets/sheet1.xml": sheet,
+ }
+ for name, content := range files {
+ w, err := zw.Create(name)
+ if err != nil {
+ return nil, err
+ }
+ if _, err := w.Write([]byte(content)); err != nil {
+ return nil, err
+ }
+ }
+ if err := zw.Close(); err != nil {
+ return nil, err
+ }
+ return buf.Bytes(), nil
+}
+
+func columnLetter(idx int) string {
+ letters := "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
+ if idx < 26 {
+ return string(letters[idx])
+ }
+ return string(letters[idx/26-1]) + string(letters[idx%26])
+}
+
+func escapeXML(s string) string {
+ out := bytes.Buffer{}
+ for _, r := range s {
+ switch r {
+ case '&':
+ out.WriteString("&")
+ case '<':
+ out.WriteString("<")
+ case '>':
+ out.WriteString(">")
+ default:
+ out.WriteRune(r)
+ }
+ }
+ return out.String()
+}
diff --git a/dms/internal/testcorpus/pdf.go b/dms/internal/testcorpus/pdf.go
new file mode 100644
index 00000000..df1f5410
--- /dev/null
+++ b/dms/internal/testcorpus/pdf.go
@@ -0,0 +1,42 @@
+// Package testcorpus erzeugt einen synthetischen, reproduzierbaren
+// Referenzkorpus fuer QA-01 (Testpyramide/Testdatenbestand). Alle Inhalte
+// sind frei erfunden (Firmennamen, Personen, Betraege) - keine Ableitung aus
+// echten Kunden-/Mandantendaten (DSGVO-Vorgabe des Projekts).
+package testcorpus
+
+import "fmt"
+
+// GeneratePDF baut ein minimal gueltiges PDF (Struktur wie thumbnail_test.go
+// minimalValidPDF) mit einem eingebetteten Textinhalt in der Content-Stream.
+func GeneratePDF(title string, lines []string) []byte {
+ content := "BT /F1 12 Tf 50 700 Td\n"
+ for _, l := range lines {
+ content += fmt.Sprintf("(%s) Tj 0 -18 Td\n", escapePDFString(l))
+ }
+ content += "ET"
+
+ obj4 := fmt.Sprintf("4 0 obj<>stream\n%s\nendstream endobj\n", len(content), content)
+
+ pdf := "%PDF-1.4\n" +
+ "1 0 obj<>endobj\n" +
+ "2 0 obj<>endobj\n" +
+ "3 0 obj<>>>>>endobj\n" +
+ obj4 +
+ "5 0 obj<>endobj\n" +
+ "xref\n0 6\n0000000000 65535 f \n" +
+ "trailer<>>>\n" +
+ "startxref\n0\n%%EOF"
+
+ return []byte(pdf)
+}
+
+func escapePDFString(s string) string {
+ out := make([]byte, 0, len(s))
+ for _, b := range []byte(s) {
+ if b == '(' || b == ')' || b == '\\' {
+ out = append(out, '\\')
+ }
+ out = append(out, b)
+ }
+ return string(out)
+}
diff --git a/dms/internal/testcorpus/tiff.go b/dms/internal/testcorpus/tiff.go
new file mode 100644
index 00000000..c5947826
--- /dev/null
+++ b/dms/internal/testcorpus/tiff.go
@@ -0,0 +1,52 @@
+package testcorpus
+
+import (
+ "bytes"
+ "encoding/binary"
+)
+
+// GenerateTIFF baut ein minimal gueltiges Baseline-TIFF (unkomprimiert,
+// 1 Byte/Pixel Graustufen) als Ersatz fuer einen eingescannten Beleg.
+// Byteorder little-endian ("II"), eine einzige IFD, ein Strip.
+func GenerateTIFF(width, height int, fill byte) []byte {
+ pixelData := bytes.Repeat([]byte{fill}, width*height)
+
+ const numTags = 8
+ ifdOffset := uint32(8)
+ ifdSize := uint32(2 + numTags*12 + 4)
+ pixelOffset := ifdOffset + ifdSize
+
+ buf := &bytes.Buffer{}
+ buf.WriteString("II")
+ binary.Write(buf, binary.LittleEndian, uint16(42))
+ binary.Write(buf, binary.LittleEndian, ifdOffset)
+
+ binary.Write(buf, binary.LittleEndian, uint16(numTags))
+
+ writeTag := func(tag, typ uint16, count, value uint32) {
+ binary.Write(buf, binary.LittleEndian, tag)
+ binary.Write(buf, binary.LittleEndian, typ)
+ binary.Write(buf, binary.LittleEndian, count)
+ binary.Write(buf, binary.LittleEndian, value)
+ }
+
+ const (
+ typeShort = 3
+ typeLong = 4
+ )
+
+ writeTag(256, typeLong, 1, uint32(width)) // ImageWidth
+ writeTag(257, typeLong, 1, uint32(height)) // ImageLength
+ writeTag(258, typeShort, 1, 8) // BitsPerSample
+ writeTag(259, typeShort, 1, 1) // Compression: none
+ writeTag(262, typeShort, 1, 1) // PhotometricInterpretation: BlackIsZero
+ writeTag(273, typeLong, 1, pixelOffset) // StripOffsets
+ writeTag(277, typeShort, 1, 1) // SamplesPerPixel
+ writeTag(279, typeLong, 1, uint32(len(pixelData))) // StripByteCounts
+
+ binary.Write(buf, binary.LittleEndian, uint32(0)) // naechste IFD: keine
+
+ buf.Write(pixelData)
+
+ return buf.Bytes()
+}
diff --git a/dms/scripts/check-security-test-coverage.sh b/dms/scripts/check-security-test-coverage.sh
index 28aa61f0..79319869 100755
--- a/dms/scripts/check-security-test-coverage.sh
+++ b/dms/scripts/check-security-test-coverage.sh
@@ -1,9 +1,11 @@
#!/usr/bin/env bash
-# Merge-Gate FDN-07 Akzeptanzkriterium 5: sicherheitsrelevante Pakete
-# (Verschluesselung, Tenant-Scoping/Speicher, Berechtigungspruefung) muessen
-# mindestens eine Testdatei mit mindestens einer func Test... besitzen.
-# Kein eigenstaendiges Auth-Paket vorhanden (noch nicht implementiert) -
-# Liste bei dessen Einfuehrung ergaenzen.
+# Merge-Gate FDN-07 Akzeptanzkriterium 5 / QA-01 Akzeptanzkriterium 4:
+# sicherheitsrelevante Pakete (Verschluesselung, Tenant-Scoping/Speicher,
+# Berechtigungspruefung, Auth-nahe Logik) muessen mindestens eine Testdatei
+# mit mindestens einer func Test... besitzen. Liste um documentapi/upload/
+# versioning erweitert (QA-01, echte Tenant-Scoping-Treffer per grep
+# bestaetigt). Kein eigenstaendiges Auth-Paket vorhanden (noch nicht
+# implementiert) - Liste bei dessen Einfuehrung ergaenzen.
set -euo pipefail
SECURITY_PACKAGES=(
@@ -12,6 +14,9 @@ SECURITY_PACKAGES=(
"internal/downloadapi"
"internal/versioningapi"
"internal/audittrail"
+ "internal/documentapi"
+ "internal/upload"
+ "internal/versioning"
)
cd "$(dirname "$0")/.."