QA-01: teststrategie-core (pflichttest-gate, testdb-isolationsnachweis, ci-workflow)
This commit is contained in:
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user