Testsuite für Import-Scheduler, Anhangsverarbeitung und Regelwerk, inklusive Tenant-Scoping und nicht-konformer Server. - tenant_scoping_test.go (imapimport + mailrules): schließt eine echte Lücke — kein bestehender Test bewies bislang explizit, dass zwei Mandanten (identischer Postfachname bzw. fehlende eigene Regel) sich nicht gegenseitig beeinflussen. - importtestgate/gate.go: echtes, ausführbares Gate (spiegelt qagate/ QA-03) — RunTestSuites liefert realen Testabdeckungsbericht (go test -cover) je Importpfad, ScanForExternalMailboxReferences bestätigt automatisiert, dass keine Testdatei einen echten externen IMAP- Anbieter referenziert. - Echten Bug beim eigenen Testlauf gefunden und behoben: die t.Cleanup-Löschfilter in scheduler_test.go/engine_test.go waren ticket- statt paketspezifisch (mandant-imp01-%/mandant-imp03-%) — die neuen IMP-09-Tenant-Testdaten wurden nie aufgeräumt, ein zweiter Testlauf schlug real mit falschen Zählungen fehl. Auf mandant-% verallgemeinert. Prüfungen (alle real durchgeführt, siehe mail/docs/IMP-09-PRUEFPROTOKOLL.md): 1. TestRun_RealGateAgainstImportPackages: realer Abdeckungsbericht imapimport 81.5%, attachments 94.4%, mailrules 71.2%. 2. go test -count=1 zweimal hintereinander real grün (reproduzierbar nach Cleanup-Fix). 3. TestScanForExternalMailboxReferences_RealImportPackagesPass: real keine externe Postfach-Referenz in den Testsuiten. Kein Umbau der geprüften Produktionslogik. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HhgFcLS8tYMhDJpP74C6AQ
170 lines
5.4 KiB
Go
170 lines
5.4 KiB
Go
// Package importtestgate implementiert IMP-09: die Import-Testsuite als
|
|
// echtes, ausführbares Prüfgate — spiegelt das Muster aus
|
|
// mail/internal/qagate (QA-03), hier bezogen auf die Import-Pfade
|
|
// (Scheduler, Anhangsverarbeitung, Regelwerk) statt Archivierung/Suche.
|
|
package importtestgate
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// ImportPackages sind die drei Importpfade, deren Testsuiten das Gate
|
|
// ausführt (Akzeptanzkriterium 1: Scheduler, Anhangsverarbeitung,
|
|
// Regelwerk).
|
|
var ImportPackages = []string{
|
|
"./internal/imapimport/...",
|
|
"./internal/attachments/...",
|
|
"./internal/mailrules/...",
|
|
}
|
|
|
|
// coverageLineRE erkennt die von `go test -cover` je Paket ausgegebene
|
|
// Zeile, z. B. "ok .../imapimport 0.45s coverage: 78.3% of statements".
|
|
var coverageLineRE = regexp.MustCompile(`^(ok|FAIL)\s+(\S+)\s.*?coverage:\s([\d.]+)% of statements`)
|
|
|
|
// PackageCoverage ist das Abdeckungsergebnis eines einzelnen Pakets.
|
|
type PackageCoverage struct {
|
|
Package string
|
|
Percent float64
|
|
TestsFailed bool
|
|
}
|
|
|
|
// TestSuiteResult ist das Ergebnis eines `go test -cover`-Laufs.
|
|
type TestSuiteResult struct {
|
|
Passed bool
|
|
Output string
|
|
Coverage []PackageCoverage
|
|
}
|
|
|
|
// RunTestSuites führt `go test -cover` über ImportPackages aus
|
|
// (Akzeptanzkriterium 1: Testabdeckungsbericht) und liefert je Paket
|
|
// Bestehen + Abdeckungsprozentsatz.
|
|
func RunTestSuites(ctx context.Context, moduleDir string) (TestSuiteResult, error) {
|
|
args := append([]string{"test", "-count=1", "-cover"}, ImportPackages...)
|
|
cmd := exec.CommandContext(ctx, "go", args...)
|
|
cmd.Dir = moduleDir
|
|
var out bytes.Buffer
|
|
cmd.Stdout = &out
|
|
cmd.Stderr = &out
|
|
runErr := cmd.Run()
|
|
|
|
result := TestSuiteResult{Output: out.String()}
|
|
if runErr != nil {
|
|
if _, isExitErr := runErr.(*exec.ExitError); !isExitErr {
|
|
return TestSuiteResult{}, fmt.Errorf("importtestgate: go test ausführen: %w", runErr)
|
|
}
|
|
}
|
|
result.Passed = runErr == nil
|
|
|
|
for _, line := range strings.Split(result.Output, "\n") {
|
|
m := coverageLineRE.FindStringSubmatch(line)
|
|
if m == nil {
|
|
continue
|
|
}
|
|
pct, err := strconv.ParseFloat(m[3], 64)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
result.Coverage = append(result.Coverage, PackageCoverage{
|
|
Package: m[2],
|
|
Percent: pct,
|
|
TestsFailed: m[1] == "FAIL",
|
|
})
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
// externalHostPatterns sind Zeichenfolgen, deren Vorkommen in einer
|
|
// Testdatei auf einen echten, externen Mailserver statt eines lokalen
|
|
// Fakes/Testservers hindeuten würde (Akzeptanzkriterium 3: reproduzierbar
|
|
// ohne echte externe Postfächer). Rein defensiv — bislang enthält keine
|
|
// Testdatei der Importpfade eine solche Zeichenfolge.
|
|
var externalHostPatterns = []string{
|
|
"imap.gmail.com", "outlook.office365.com", "imap.mail.yahoo.com", "imap.gmx.net", "imap.web.de",
|
|
}
|
|
|
|
// ScanResult ist das Ergebnis des externen-Host-Scans.
|
|
type ScanResult struct {
|
|
Passed bool
|
|
Violations []string
|
|
}
|
|
|
|
// ScanForExternalMailboxReferences prüft alle *_test.go-Dateien in
|
|
// ImportPackages auf Referenzen zu bekannten echten IMAP-Anbietern
|
|
// (Akzeptanzkriterium 3).
|
|
func ScanForExternalMailboxReferences(moduleDir string) (ScanResult, error) {
|
|
var violations []string
|
|
for _, pkgPattern := range ImportPackages {
|
|
dir := filepath.Join(moduleDir, strings.TrimSuffix(strings.TrimPrefix(pkgPattern, "./"), "/..."))
|
|
entries, err := os.ReadDir(dir)
|
|
if err != nil {
|
|
return ScanResult{}, fmt.Errorf("importtestgate: verzeichnis %s lesen: %w", dir, err)
|
|
}
|
|
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 ScanResult{}, fmt.Errorf("importtestgate: %s lesen: %w", e.Name(), err)
|
|
}
|
|
for _, host := range externalHostPatterns {
|
|
if strings.Contains(string(content), host) {
|
|
violations = append(violations, fmt.Sprintf("%s/%s: enthält externe Host-Referenz %q", dir, e.Name(), host))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return ScanResult{Passed: len(violations) == 0, Violations: violations}, nil
|
|
}
|
|
|
|
// GateResult fasst ein vollständiges IMP-09-Gate-Ergebnis zusammen.
|
|
type GateResult struct {
|
|
Timestamp time.Time
|
|
TestSuite TestSuiteResult
|
|
ExternalScan ScanResult
|
|
}
|
|
|
|
func (r GateResult) Passed() bool {
|
|
return r.TestSuite.Passed && r.ExternalScan.Passed
|
|
}
|
|
|
|
// Run führt das vollständige IMP-09-Gate aus.
|
|
func Run(ctx context.Context, moduleDir string) (GateResult, error) {
|
|
testResult, err := RunTestSuites(ctx, moduleDir)
|
|
if err != nil {
|
|
return GateResult{}, err
|
|
}
|
|
scanResult, err := ScanForExternalMailboxReferences(moduleDir)
|
|
if err != nil {
|
|
return GateResult{}, err
|
|
}
|
|
return GateResult{Timestamp: time.Now().UTC(), TestSuite: testResult, ExternalScan: scanResult}, nil
|
|
}
|
|
|
|
// Report erzeugt einen dokumentierten, zeitgestempelten Bericht
|
|
// (Akzeptanzkriterium 1: Testabdeckungsbericht liegt vor).
|
|
func (r GateResult) Report() string {
|
|
status := "BESTANDEN"
|
|
if !r.Passed() {
|
|
status = "FEHLGESCHLAGEN"
|
|
}
|
|
var b strings.Builder
|
|
fmt.Fprintf(&b, "# IMP-09 Import-Testsuite-Gate: %s\n\n", status)
|
|
fmt.Fprintf(&b, "Zeitstempel (UTC): %s\n\n", r.Timestamp.Format(time.RFC3339))
|
|
fmt.Fprintf(&b, "## Testabdeckung\n\n")
|
|
for _, c := range r.TestSuite.Coverage {
|
|
fmt.Fprintf(&b, "- %s: %.1f%% (bestanden: %v)\n", c.Package, c.Percent, !c.TestsFailed)
|
|
}
|
|
fmt.Fprintf(&b, "\n## Externe-Postfach-Scan\n\nBestanden: %v\n", r.ExternalScan.Passed)
|
|
return b.String()
|
|
}
|