QA-01: teststrategie-mail (mail-modul-grundstein)

- mail/go.mod: erstes eigenstaendiges Go-Modul fuer NEXARCH Mail
- mail/docs/TESTSTRATEGIE-MAIL.md: Testpyramide (Unit/Integration/
  Protokoll-Zustandsmaschinen/E2E/Vertragstests), Pflichttest-Merge-Gate,
  Bug-Tracking-Konvention (Gitea-Issues), analog Core QA-01
- mail/internal/example: ein reales, kleines Beispiel (Adress-
  Normalisierung) mit je einem Test pro Testart (Unit/Integration/E2E),
  6 Tests real bestanden
- mail/internal/pflichttestgate + cmd/pflichttestgate: Merge-Gate-CLI,
  echter End-zu-Ende-Beweis (Binary lehnt Verstoss ab, akzeptiert
  begleiteten Test), .gitea/workflows/mail-pflichttest-gate.yml
- Ehrlich dokumentiert: kein Gitea-API-Token verfuegbar, daher kein
  echter Issue angelegt - Bug-Tracking-Vorgehen stattdessen anhand
  eines realen, bereits dokumentierten Befunds (RET-10) durchgespielt,
  als offener Punkt vermerkt
- Gegenlesen durch zweite Person (Nutzer) noch ausstehend

Pruefungen siehe mail/docs/TESTSTRATEGIE-MAIL.md
This commit is contained in:
sysops
2026-08-30 23:24:37 +02:00
parent c895a67c4b
commit 44b78b1554
13 changed files with 581 additions and 0 deletions
+72
View File
@@ -0,0 +1,72 @@
// Package pflichttestgate erzwingt die in docs/TESTSTRATEGIE-MAIL.md
// Abschnitt 4 festgelegte Regel: jede geänderte Go-Datei in einem
// sicherheitskritischen Bereich (Auth, Tenant-Scoping, Protokoll-/
// Compliance-kritisch) muss von einer geänderten oder neuen _test.go-
// Datei im selben Package begleitet sein. Bewusste Code-Kopie des
// Musters aus Core internal/pflichttestgate — Mail ist ein eigenständiges
// Go-Modul und kann Core nicht importieren.
package pflichttestgate
import (
"path"
"regexp"
"strings"
)
// sensitivePathPatterns beschreibt die Bereiche aus
// TESTSTRATEGIE-MAIL.md Abschnitt 4.
var sensitivePathPatterns = []*regexp.Regexp{
regexp.MustCompile(`(^|/)mail/internal/auth/`),
regexp.MustCompile(`(^|/)mail/internal/tenant/`),
regexp.MustCompile(`(^|/)mail/internal/ingest/`),
regexp.MustCompile(`(^|/)mail/internal/imap/`),
regexp.MustCompile(`(^|/)mail/internal/smtp/`),
regexp.MustCompile(`(^|/)mail/internal/arc/`),
}
// Violation beschreibt ein Package mit sicherheitskritischer Änderung
// ohne begleitende Testdatei.
type Violation struct {
Package string
ChangedFile string
}
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 gegen die
// Pflichttest-Regel — 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,33 @@
// Negativtest des Gates selbst (QA-01 Prüfung 1): ein Diff mit
// geänderter mail/internal/auth/login.go ohne begleitende Testdatei
// muss als Verstoß erkannt werden.
package pflichttestgate
import "testing"
func TestCheckDiff_FlagsSensitiveChangeWithoutTest(t *testing.T) {
violations := CheckDiff([]string{"mail/internal/auth/login.go"})
if len(violations) != 1 {
t.Fatalf("erwartet genau 1 verstoß, habe %d: %+v", len(violations), violations)
}
if violations[0].Package != "mail/internal/auth" {
t.Fatalf("falsches package gemeldet: %+v", violations[0])
}
}
func TestCheckDiff_PassesWhenTestFileAccompanies(t *testing.T) {
violations := CheckDiff([]string{
"mail/internal/auth/login.go",
"mail/internal/auth/login_test.go",
})
if len(violations) != 0 {
t.Fatalf("erwartet keine verstöße, habe: %+v", violations)
}
}
func TestCheckDiff_IgnoresNonSensitivePaths(t *testing.T) {
violations := CheckDiff([]string{"mail/internal/example/normalize.go"})
if len(violations) != 0 {
t.Fatalf("erwartet keine verstöße für nicht-sensiblen pfad, habe: %+v", violations)
}
}