Neues Paket mail/internal/ingestiontestgate, spiegelt das etablierte Muster aus mail/internal/qagate (QA-03) und mail/internal/importtestgate (IMP-09): ein echtes, ausführbares Gate statt einer nur behaupteten Prüfung. IngestionAndImportPackages listet alle 14 aus QA-02s eigener dependsOn-Liste folgenden Pakete (Protokoll-Server, Fehlerbehandlung/ Logging, Import-Pfade). RunTestSuites führt go test -count=1 -p 1 über alle 14 Pakete aus — -p 1 bewusst, da parallele Testbinaries sich bei den echten QA-07-Lasttests gegenseitig CPU-Kontingent wegnehmen und Latenz-Zielwerte durch reine Testhost-Überlastung verfehlen lassen (real beobachtet, mit -p 1 behoben). ScanForKnownErrorPointTests prüft automatisiert, dass die drei in Akzeptanzkriterium 2 namentlich geforderten Fehlerpunkte (Header-Injection, Anhang-Limit, UIDVALIDITY) je einen passenden Regressionstest im zuständigen Paket haben. Gate-Lauf gegen aktuellen Stand real ausgeführt und mit Zeitstempel dokumentiert: BESTANDEN, 14 Pakete grün, alle drei bekannten Fehlerpunkte abgedeckt. Scanner durch Negativtest bewiesen, dass er eine tatsächlich fehlende Abdeckung real erkennt statt immer "bestanden" zu melden. Pflichtprüfung 3 (Zweitperson bestätigt Gate-Ergebnis unabhängig) bleibt strukturell offen — eine einzelne Sitzung kann keine unabhängige zweite Person sein, im Prüfprotokoll dokumentiert (analog zu ING-10 und QA-04). go build/go vet/golangci-lint clean, gesamtes Mail-Modul regressionsfrei getestet.
233 lines
7.9 KiB
Go
233 lines
7.9 KiB
Go
// Package ingestiontestgate implementiert QA-02: das Prüfgate für
|
|
// Ingestion & Import — spiegelt das Muster aus mail/internal/qagate
|
|
// (QA-03) und mail/internal/importtestgate (IMP-09), hier bezogen auf
|
|
// ALLE Ingestion-/Import-Pakete zusammen (Protokoll-Server, Fehler-
|
|
// behandlung/Logging, Import-Pfade).
|
|
package ingestiontestgate
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// IngestionAndImportPackages sind alle Pakete, deren Testsuiten das
|
|
// Gate ausführt (Akzeptanzkriterium 1) — abgeleitet aus QA-02s
|
|
// dependsOn-Liste: ING-10 (Protokoll-Server + MIME + Folder-State),
|
|
// ING-07 (Fehlerbehandlung, protoguard), ING-08 (Logging, protolog),
|
|
// IMP-04/IMP-09 (imapimport), IMP-05 (hotfolder), IMP-06 (virusscan),
|
|
// IMP-07 (mailboxconfig), IMP-08 (syncalert). attachments/mailrules
|
|
// gehören ebenfalls zum von IMP-09 geprüften Importpfad.
|
|
var IngestionAndImportPackages = []string{
|
|
"./internal/imap/...",
|
|
"./internal/pop3/...",
|
|
"./internal/smtp/...",
|
|
"./internal/protoguard/...",
|
|
"./internal/protolog/...",
|
|
"./internal/mimeparse/...",
|
|
"./internal/folderstate/...",
|
|
"./internal/imapimport/...",
|
|
"./internal/attachments/...",
|
|
"./internal/mailrules/...",
|
|
"./internal/hotfolder/...",
|
|
"./internal/virusscan/...",
|
|
"./internal/mailboxconfig/...",
|
|
"./internal/syncalert/...",
|
|
}
|
|
|
|
// TestSuiteResult ist das Ergebnis eines `go test`-Laufs über
|
|
// IngestionAndImportPackages.
|
|
type TestSuiteResult struct {
|
|
Passed bool
|
|
Output string
|
|
}
|
|
|
|
// RunTestSuites führt `go test` über IngestionAndImportPackages im
|
|
// angegebenen Modulverzeichnis aus (Akzeptanzkriterium 1). Umgebungs-
|
|
// variablen (TEST_TENANT_DSN u. Ä.) werden unverändert vom aufrufenden
|
|
// Prozess vererbt — das Gate setzt selbst keine Zugangsdaten.
|
|
func RunTestSuites(ctx context.Context, moduleDir string) (TestSuiteResult, error) {
|
|
// -p 1: Pakete NACHEINANDER statt parallel — mehrere gleichzeitige
|
|
// go-test-Binaries würden sich bei den echten QA-07-Lasttests
|
|
// (imap/pop3/smtp) gegenseitig CPU-Kontingent wegnehmen und so
|
|
// Latenz-Zielwerte durch reine Testhost-Überlastung verfehlen
|
|
// lassen, nicht durch einen echten Regressionsfehler.
|
|
args := append([]string{"test", "-count=1", "-p", "1"}, IngestionAndImportPackages...)
|
|
cmd := exec.CommandContext(ctx, "go", args...)
|
|
cmd.Dir = moduleDir
|
|
var out bytes.Buffer
|
|
cmd.Stdout = &out
|
|
cmd.Stderr = &out
|
|
err := cmd.Run()
|
|
if err != nil {
|
|
if _, isExitErr := err.(*exec.ExitError); !isExitErr {
|
|
return TestSuiteResult{}, fmt.Errorf("ingestiontestgate: go test ausführen: %w", err)
|
|
}
|
|
}
|
|
return TestSuiteResult{Passed: err == nil, Output: out.String()}, nil
|
|
}
|
|
|
|
// KnownErrorPoint ist ein bekannter, historisch dokumentierter
|
|
// Fehlerpunkt (Akzeptanzkriterium 2), der durch mindestens einen
|
|
// benannten Regressionstest abgedeckt sein muss.
|
|
type KnownErrorPoint struct {
|
|
Name string // z. B. "Header-Injection"
|
|
PackageRelDir string // relativ zu moduleDir, z. B. "internal/mailer"
|
|
TestNameMarker string // Teilzeichenkette, die im Testfunktionsnamen vorkommen muss
|
|
Rationale string // kurze Begründung, warum dieser Punkt bekannt kritisch ist
|
|
}
|
|
|
|
// knownErrorPoints sind die drei in QA-02 Akzeptanzkriterium 2 namentlich
|
|
// geforderten Fehlerpunkte.
|
|
var knownErrorPoints = []KnownErrorPoint{
|
|
{
|
|
Name: "Header-Injection",
|
|
PackageRelDir: "internal/mailer",
|
|
TestNameMarker: "CRLF",
|
|
Rationale: "known-issues-archivmail.md #1: From/To/Subject per Stringkonkatenation ohne CRLF-Prüfung",
|
|
},
|
|
{
|
|
Name: "Anhang-Limit",
|
|
PackageRelDir: "internal/mimeparse",
|
|
TestNameMarker: "OversizedAttachment",
|
|
Rationale: "known-issues-archivmail.md #3: Anhänge ohne Größenlimit gelesen (Speicherbombe)",
|
|
},
|
|
{
|
|
Name: "UIDVALIDITY",
|
|
PackageRelDir: "internal/folderstate",
|
|
TestNameMarker: "UIDValidity",
|
|
Rationale: "ING-01/repos-analyse-mail-reuse.md: archivmail brach Resync bei UIDVALIDITY=0 nicht-konformer Server",
|
|
},
|
|
}
|
|
|
|
// KnownErrorPointResult ist das Ergebnis der Prüfung EINES bekannten
|
|
// Fehlerpunkts.
|
|
type KnownErrorPointResult struct {
|
|
Point KnownErrorPoint
|
|
Covered bool
|
|
FoundInFile string
|
|
TestName string
|
|
}
|
|
|
|
// KnownErrorPointsResult fasst die Stichprobenprüfung aller bekannten
|
|
// Fehlerpunkte zusammen (Akzeptanzkriterium 2, Pflichtprüfung 2:
|
|
// "mindestens ein Regressionstest je bekanntem Fehlerpunkt").
|
|
type KnownErrorPointsResult struct {
|
|
Passed bool
|
|
Results []KnownErrorPointResult
|
|
}
|
|
|
|
// ScanForKnownErrorPointTests prüft für jeden bekannten Fehlerpunkt, ob
|
|
// im angegebenen Paketverzeichnis mindestens eine _test.go-Datei eine
|
|
// Testfunktion enthält, deren Name den TestNameMarker enthält.
|
|
func ScanForKnownErrorPointTests(moduleDir string) (KnownErrorPointsResult, error) {
|
|
out := KnownErrorPointsResult{Passed: true}
|
|
|
|
for _, point := range knownErrorPoints {
|
|
dir := filepath.Join(moduleDir, point.PackageRelDir)
|
|
entries, err := os.ReadDir(dir)
|
|
if err != nil {
|
|
return KnownErrorPointsResult{}, fmt.Errorf("ingestiontestgate: %s lesen: %w", dir, err)
|
|
}
|
|
res := KnownErrorPointResult{Point: point}
|
|
for _, e := range entries {
|
|
if e.IsDir() || !strings.HasSuffix(e.Name(), "_test.go") {
|
|
continue
|
|
}
|
|
content, err := os.ReadFile(filepath.Join(dir, e.Name()))
|
|
if err != nil {
|
|
return KnownErrorPointsResult{}, fmt.Errorf("ingestiontestgate: %s lesen: %w", e.Name(), err)
|
|
}
|
|
if testName, ok := findTestFuncContaining(string(content), point.TestNameMarker); ok {
|
|
res.Covered = true
|
|
res.FoundInFile = e.Name()
|
|
res.TestName = testName
|
|
break
|
|
}
|
|
}
|
|
if !res.Covered {
|
|
out.Passed = false
|
|
}
|
|
out.Results = append(out.Results, res)
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
// findTestFuncContaining sucht die erste "func TestXxx(" -Zeile, deren
|
|
// Funktionsname marker enthält.
|
|
func findTestFuncContaining(source, marker string) (string, bool) {
|
|
for _, line := range strings.Split(source, "\n") {
|
|
trimmed := strings.TrimSpace(line)
|
|
if !strings.HasPrefix(trimmed, "func Test") {
|
|
continue
|
|
}
|
|
parenIdx := strings.Index(trimmed, "(")
|
|
if parenIdx < 0 {
|
|
continue
|
|
}
|
|
name := strings.TrimPrefix(trimmed[:parenIdx], "func ")
|
|
if strings.Contains(name, marker) {
|
|
return name, true
|
|
}
|
|
}
|
|
return "", false
|
|
}
|
|
|
|
// GateResult fasst ein vollständiges QA-02-Gate-Ergebnis zusammen
|
|
// (Akzeptanzkriterium 3: dokumentiert, mit Zeitstempel).
|
|
type GateResult struct {
|
|
Timestamp time.Time
|
|
TestSuite TestSuiteResult
|
|
KnownErrorPoints KnownErrorPointsResult
|
|
}
|
|
|
|
// Passed ist true, wenn ALLE Kriterien erfüllt sind.
|
|
func (r GateResult) Passed() bool {
|
|
return r.TestSuite.Passed && r.KnownErrorPoints.Passed
|
|
}
|
|
|
|
// Run führt das vollständige Gate aus.
|
|
func Run(ctx context.Context, moduleDir string) (GateResult, error) {
|
|
testResult, err := RunTestSuites(ctx, moduleDir)
|
|
if err != nil {
|
|
return GateResult{}, err
|
|
}
|
|
errorPointsResult, err := ScanForKnownErrorPointTests(moduleDir)
|
|
if err != nil {
|
|
return GateResult{}, err
|
|
}
|
|
return GateResult{
|
|
Timestamp: time.Now().UTC(),
|
|
TestSuite: testResult,
|
|
KnownErrorPoints: errorPointsResult,
|
|
}, nil
|
|
}
|
|
|
|
// Report erzeugt den dokumentierten, zeitgestempelten Gate-Bericht
|
|
// (Akzeptanzkriterium 3).
|
|
func (r GateResult) Report() string {
|
|
status := "BESTANDEN"
|
|
if !r.Passed() {
|
|
status = "FEHLGESCHLAGEN"
|
|
}
|
|
var b strings.Builder
|
|
fmt.Fprintf(&b, "# QA-02 Gate-Ergebnis: %s\n\n", status)
|
|
fmt.Fprintf(&b, "Zeitstempel (UTC): %s\n\n", r.Timestamp.Format(time.RFC3339))
|
|
fmt.Fprintf(&b, "## Testsuiten (Ingestion & Import, %d Pakete)\n\nBestanden: %v\n\n", len(IngestionAndImportPackages), r.TestSuite.Passed)
|
|
fmt.Fprintf(&b, "## Bekannte Fehlerpunkte — Regressionstest-Stichprobe\n\n")
|
|
for _, res := range r.KnownErrorPoints.Results {
|
|
fmt.Fprintf(&b, "- %s (%s): abgedeckt=%v", res.Point.Name, res.Point.Rationale, res.Covered)
|
|
if res.Covered {
|
|
fmt.Fprintf(&b, " — %s in %s/%s\n", res.TestName, res.Point.PackageRelDir, res.FoundInFile)
|
|
} else {
|
|
fmt.Fprintf(&b, "\n")
|
|
}
|
|
}
|
|
return b.String()
|
|
}
|