QA-01: teststrategie & testdatenbestand
CI / Core-Modul (Build/Vet/Test) (push) Failing after 3s
CI / DMS Security-Test-Gate (AC5, Merge-Gate) (push) Successful in 2s
CI / DMS-Modul Supply-Chain-Scan (govulncheck) (push) Failing after 58s
CI / Web (dms-browser) (push) Failing after 51s
CI / Web (pdf-viewer) (push) Failing after 45s
CI / Web (shl) (push) Failing after 20s
CI / DMS-Modul (Lint/Build/Test/Coverage/Artefakt) (push) Failing after 2s
CI / Core-Modul (Build/Vet/Test) (push) Failing after 3s
CI / DMS Security-Test-Gate (AC5, Merge-Gate) (push) Successful in 2s
CI / DMS-Modul Supply-Chain-Scan (govulncheck) (push) Failing after 58s
CI / Web (dms-browser) (push) Failing after 51s
CI / Web (pdf-viewer) (push) Failing after 45s
CI / Web (shl) (push) Failing after 20s
CI / DMS-Modul (Lint/Build/Test/Coverage/Artefakt) (push) Failing after 2s
Testpyramide, Abdeckungsziele und verbindliches Security-Merge-Gate schriftlich festgelegt (dms/docs/QA-01-TESTSTRATEGIE.md). Merge-Gate aus FDN-07 (check-security-test-coverage.sh) um drei real identifizierte, tenant-bezogene Pakete erweitert (documentapi, upload, versioning). Reproduzierbarer, vollstaendig synthetischer Referenzkorpus (PDF, Office, TIFF-Scan-Ersatz) ueber dms/internal/testcorpus + go run ./cmd/gen-test-corpus, mit Test, der Formatabdeckung und Parsbarkeit der erzeugten Dateien prueft. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
cbeff53ec5
commit
4e7ef754a0
8 files changed
+551
-5
No files matched your search
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
package testcorpus
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"bytes"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
const contentTypesDocx = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">
|
||||
<Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>
|
||||
<Default Extension="xml" ContentType="application/xml"/>
|
||||
<Override PartName="/word/document.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml"/>
|
||||
</Types>`
|
||||
|
||||
const rootRelsDocx = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
|
||||
<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="word/document.xml"/>
|
||||
</Relationships>`
|
||||
|
||||
const documentXMLTemplate = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
|
||||
<w:body>%s</w:body>
|
||||
</w:document>`
|
||||
|
||||
// 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("<w:p><w:r><w:t>%s</w:t></w:r></w:p>", 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 = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">
|
||||
<Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>
|
||||
<Default Extension="xml" ContentType="application/xml"/>
|
||||
<Override PartName="/xl/workbook.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml"/>
|
||||
<Override PartName="/xl/worksheets/sheet1.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml"/>
|
||||
</Types>`
|
||||
|
||||
const rootRelsXlsx = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
|
||||
<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="xl/workbook.xml"/>
|
||||
</Relationships>`
|
||||
|
||||
const workbookXML = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<workbook xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">
|
||||
<sheets><sheet name="Tabelle1" sheetId="1" r:id="rId1"/></sheets>
|
||||
</workbook>`
|
||||
|
||||
const workbookRels = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
|
||||
<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet" Target="worksheets/sheet1.xml"/>
|
||||
</Relationships>`
|
||||
|
||||
// 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 := `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main"><sheetData>`
|
||||
for r, row := range rows {
|
||||
sheet += fmt.Sprintf(`<row r="%d">`, r+1)
|
||||
for c, cell := range row {
|
||||
ref := fmt.Sprintf("%s%d", columnLetter(c), r+1)
|
||||
sheet += fmt.Sprintf(`<c r="%s" t="inlineStr"><is><t>%s</t></is></c>`, ref, escapeXML(cell))
|
||||
}
|
||||
sheet += `</row>`
|
||||
}
|
||||
sheet += `</sheetData></worksheet>`
|
||||
|
||||
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()
|
||||
}
|
||||
@@ -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<</Length %d>>stream\n%s\nendstream endobj\n", len(content), content)
|
||||
|
||||
pdf := "%PDF-1.4\n" +
|
||||
"1 0 obj<</Type/Catalog/Pages 2 0 R>>endobj\n" +
|
||||
"2 0 obj<</Type/Pages/Kids[3 0 R]/Count 1>>endobj\n" +
|
||||
"3 0 obj<</Type/Page/Parent 2 0 R/MediaBox[0 0 595 842]/Contents 4 0 R/Resources<</Font<</F1 5 0 R>>>>>>endobj\n" +
|
||||
obj4 +
|
||||
"5 0 obj<</Type/Font/Subtype/Type1/BaseFont/Helvetica>>endobj\n" +
|
||||
"xref\n0 6\n0000000000 65535 f \n" +
|
||||
"trailer<</Size 6/Root 1 0 R/Info<</Title(" + escapePDFString(title) + ")>>>>\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)
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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")/.."
|
||||
|
||||
Reference in new issue
Block a user