// 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() }