QA-01: teststrategie-core (pflichttest-gate, testdb-isolationsnachweis, ci-workflow)

This commit is contained in:
sysops
2026-08-28 22:41:06 +02:00
parent c895a67c4b
commit 5a0d07444c
7 changed files with 468 additions and 0 deletions
+67
View File
@@ -0,0 +1,67 @@
// Package pflichttestgate erzwingt die in docs/TESTSTRATEGIE-CORE.md Abschnitt 3 festgelegte Regel:
// jede geänderte Go-Datei in einem sicherheitskritischen Bereich (Auth, Tenant-Scoping, Policy)
// muss von einer geänderten oder neuen _test.go-Datei im selben Package begleitet sein.
package pflichttestgate
import (
"path"
"regexp"
"strings"
)
// sensitivePathPatterns beschreibt die Bereiche aus TESTSTRATEGIE-CORE.md Abschnitt 3.
var sensitivePathPatterns = []*regexp.Regexp{
regexp.MustCompile(`(^|/)internal/auth/`),
regexp.MustCompile(`(^|/)internal/iam/`),
regexp.MustCompile(`(^|/)internal/tenant/`),
regexp.MustCompile(`(^|/)internal/rbac/`),
regexp.MustCompile(`(^|/)internal/policy/`),
}
// Violation beschreibt ein Package mit sicherheitskritischer Änderung ohne begleitende Testdatei.
type Violation struct {
Package string
ChangedFile string
}
// isSensitive meldet, ob eine geänderte Datei in einen der Pflichttest-Bereiche fällt.
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 (z. B. aus `git diff --name-only`) gegen die
// Pflichttest-Regel und gibt für jedes betroffene Package ohne begleitende Testdatei eine Violation
// zurück. Ein leeres Ergebnis bedeutet: Gate besteht.
func CheckDiff(changedFiles []string) []Violation {
sensitiveByPkg := map[string]string{} // package -> erste sensible Datei (fürs Reporting)
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
}
+84
View File
@@ -0,0 +1,84 @@
package pflichttestgate
import "testing"
// Prüfung 1 (QA-01): CI-Lauf mit absichtlich fehlendem Pflichttest schlägt kontrolliert fehl —
// Negativtest des Gates selbst.
func TestCheckDiff_BlocksAuthChangeWithoutTest(t *testing.T) {
changed := []string{
"internal/auth/login.go",
"internal/apiserver/handler.go", // unkritisch, keine Testpflicht
}
violations := CheckDiff(changed)
if len(violations) != 1 {
t.Fatalf("erwartet 1 Verstoß, bekommen %d: %+v", len(violations), violations)
}
if violations[0].Package != "internal/auth" {
t.Errorf("erwartetes Package internal/auth, bekommen %q", violations[0].Package)
}
if violations[0].ChangedFile != "internal/auth/login.go" {
t.Errorf("erwartete Datei internal/auth/login.go, bekommen %q", violations[0].ChangedFile)
}
}
func TestCheckDiff_PassesWhenTestFileAccompaniesChange(t *testing.T) {
changed := []string{
"internal/auth/login.go",
"internal/auth/login_test.go",
}
violations := CheckDiff(changed)
if len(violations) != 0 {
t.Fatalf("erwartet keinen Verstoß, bekommen %+v", violations)
}
}
func TestCheckDiff_PassesForNewTestFileEvenWithoutSourceChange(t *testing.T) {
// Nachtraeglich ergaenzter Test fuer bestehenden Code ist erlaubt/erwuenscht.
changed := []string{"internal/tenant/registry_test.go"}
violations := CheckDiff(changed)
if len(violations) != 0 {
t.Fatalf("erwartet keinen Verstoß, bekommen %+v", violations)
}
}
func TestCheckDiff_IgnoresUnrelatedPackages(t *testing.T) {
changed := []string{"internal/config/config.go", "internal/db/db.go"}
violations := CheckDiff(changed)
if len(violations) != 0 {
t.Fatalf("erwartet keinen Verstoß fuer unkritische Pakete, bekommen %+v", violations)
}
}
func TestCheckDiff_CoversAllSensitiveAreas(t *testing.T) {
cases := []string{
"internal/auth/session.go",
"internal/iam/user.go",
"internal/tenant/provisioner.go",
"internal/rbac/enforcer.go",
"internal/policy/decision.go",
}
for _, f := range cases {
violations := CheckDiff([]string{f})
if len(violations) != 1 {
t.Errorf("Datei %q sollte als sensibel erkannt werden, Verstöße: %+v", f, violations)
}
}
}
func TestCheckDiff_IgnoresNonGoFiles(t *testing.T) {
changed := []string{"internal/auth/README.md"}
violations := CheckDiff(changed)
if len(violations) != 0 {
t.Fatalf("Nicht-Go-Dateien duerfen keine Testpflicht ausloesen, bekommen %+v", violations)
}
}