Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b775de651b | ||
|
|
8db663b7db | ||
|
|
3f4ac77b1e | ||
|
|
0c8b3fe2b3 |
No files matched your search
@@ -0,0 +1,23 @@
|
||||
name: common Pflichttest-Gate
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- "common/**"
|
||||
|
||||
jobs:
|
||||
pflichttest-gate:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: "1.25"
|
||||
- name: Gate bauen
|
||||
working-directory: common
|
||||
run: go build -o /tmp/pflichttestgate ./cmd/pflichttestgate
|
||||
- name: Geänderte Dateien gegen Pflichttest-Regel pruefen
|
||||
run: |
|
||||
git diff --name-only "origin/${{ github.base_ref }}...HEAD" -- common/ | /tmp/pflichttestgate
|
||||
@@ -0,0 +1,28 @@
|
||||
name: DB-Integrationstests
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- "common/**"
|
||||
- "dms/**"
|
||||
- "mail/**"
|
||||
- "archive/**"
|
||||
- "ai/**"
|
||||
- "connect/**"
|
||||
- "workflow/**"
|
||||
- "internal/**"
|
||||
|
||||
jobs:
|
||||
db-tests:
|
||||
runs-on: nexarch-test
|
||||
env:
|
||||
TEST_ADMIN_DSN: ${{ secrets.TEST_ADMIN_DSN }}
|
||||
REQUIRE_DB: "1"
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: "1.25"
|
||||
- name: Tests (common)
|
||||
working-directory: common
|
||||
run: go test ./testdb/... ./migrate/... -count=1 -p 1
|
||||
+4
-1
@@ -2,7 +2,7 @@
|
||||
|
||||
Geteiltes Go-Modul für Pakete, die in mehreren NEXARCH-Modulen identisch gebraucht werden (`internal/` ist modulübergreifend nicht sichtbar).
|
||||
|
||||
Pakete: `apierror`, `auditclient`, `httpclient`, `httpharden`, `loadtest`, `migrate`, `policyclient`, `svcauth`, `tenantregistryclient`, `testdb`.
|
||||
Pakete: `pflichttestgate` (parametrisierte Testpflicht-Logik), `apierror`, `auditclient`, `httpclient`, `httpharden`, `loadtest`, `migrate`, `policyclient`, `svcauth`, `tenantregistryclient`, `testdb`.
|
||||
|
||||
## Einbinden
|
||||
|
||||
@@ -21,3 +21,6 @@ Tags `common/v0.X.Y` setzt `.gitea/workflows/common-tag.yml` nach jedem Merge in
|
||||
|
||||
- Nur Code, der in mindestens zwei Modulen byte-gleich (ohne Kommentare) gebraucht wird; fachlich verschiedene Pakete bleiben modul-lokal.
|
||||
- Änderungen abwärtskompatibel halten, solange Module nicht gleichzeitig umgestellt werden.
|
||||
|
||||
- Jeder PR an `common/` nennt die betroffenen Module (Auswirkungsliste). Breaking Changes nur mit neuer Minor-Version (`v0.2.0`) und Umstellungs-PRs je Modul.
|
||||
- Sicherheitskritische Pakete (`svcauth`, `tenantregistryclient`, `auditclient`, `policyclient`, `apierror`, `httpharden`) unterliegen dem Pflichttest-Gate (`cmd/pflichttestgate`, Workflow `common-pflichttest-gate.yml`): Änderung ohne begleitende `_test.go` im selben Package schlägt fehl.
|
||||
@@ -0,0 +1,49 @@
|
||||
// Command pflichttestgate ist das CI-Gate für common/.
|
||||
// Aufruf: pflichttestgate < geänderte-Dateien (eine pro Zeile, via stdin)
|
||||
// Exit-Code 1 mit Liste der Verstöße, wenn ein sicherheitskritisches Package ohne
|
||||
// begleitende Testdatei geändert wurde.
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"os"
|
||||
"regexp"
|
||||
|
||||
"gitea.perlbach24.de/scripte/nexarch/common/pflichttestgate"
|
||||
)
|
||||
|
||||
var sensitive = []*regexp.Regexp{
|
||||
regexp.MustCompile(`(^|/)common/svcauth/`),
|
||||
regexp.MustCompile(`(^|/)common/tenantregistryclient/`),
|
||||
regexp.MustCompile(`(^|/)common/auditclient/`),
|
||||
regexp.MustCompile(`(^|/)common/policyclient/`),
|
||||
regexp.MustCompile(`(^|/)common/apierror/`),
|
||||
regexp.MustCompile(`(^|/)common/httpharden/`),
|
||||
}
|
||||
|
||||
func main() {
|
||||
var changedFiles []string
|
||||
scanner := bufio.NewScanner(os.Stdin)
|
||||
for scanner.Scan() {
|
||||
if line := scanner.Text(); line != "" {
|
||||
changedFiles = append(changedFiles, line)
|
||||
}
|
||||
}
|
||||
if err := scanner.Err(); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "pflichttestgate: Eingabe konnte nicht gelesen werden: %v\n", err)
|
||||
os.Exit(2)
|
||||
}
|
||||
|
||||
violations := pflichttestgate.CheckDiff(changedFiles, sensitive)
|
||||
if len(violations) == 0 {
|
||||
fmt.Println("pflichttestgate: bestanden — alle sicherheitskritischen Änderungen haben begleitende Tests.")
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Fprintln(os.Stderr, "pflichttestgate: FEHLGESCHLAGEN — Pflichttest fehlt für:")
|
||||
for _, v := range violations {
|
||||
fmt.Fprintf(os.Stderr, " - Package %q (Datei %q hat keine begleitende _test.go-Änderung)\n", v.Package, v.ChangedFile)
|
||||
}
|
||||
os.Exit(1)
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
// Package pflichttestgate erzwingt: jede geänderte Go-Datei in einem sicherheitskritischen
|
||||
// Bereich muss von einer geänderten oder neuen _test.go-Datei im selben Package begleitet sein.
|
||||
// Die Bereiche (Pfadmuster) liefert der Aufrufer, die Logik ist für alle Module dieselbe.
|
||||
package pflichttestgate
|
||||
|
||||
import (
|
||||
"path"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Violation struct {
|
||||
Package string
|
||||
ChangedFile string
|
||||
}
|
||||
|
||||
func IsSensitive(file string, patterns []*regexp.Regexp) bool {
|
||||
if !strings.HasSuffix(file, ".go") || strings.HasSuffix(file, "_test.go") {
|
||||
return false
|
||||
}
|
||||
for _, re := range patterns {
|
||||
if re.MatchString(file) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func CheckDiff(changedFiles []string, patterns []*regexp.Regexp) []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, patterns) {
|
||||
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,54 @@
|
||||
package pflichttestgate
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"testing"
|
||||
)
|
||||
|
||||
var testPatterns = []*regexp.Regexp{
|
||||
regexp.MustCompile(`(^|/)common/svcauth/`),
|
||||
regexp.MustCompile(`(^|/)common/tenantregistryclient/`),
|
||||
}
|
||||
|
||||
func TestCheckDiff_BlocksSensitiveChangeWithoutTest(t *testing.T) {
|
||||
changed := []string{"common/svcauth/svcauth.go", "common/loadtest/loadtest.go"}
|
||||
|
||||
violations := CheckDiff(changed, testPatterns)
|
||||
|
||||
if len(violations) != 1 {
|
||||
t.Fatalf("erwartet 1 Verstoß, bekommen %d: %+v", len(violations), violations)
|
||||
}
|
||||
if violations[0].Package != "common/svcauth" || violations[0].ChangedFile != "common/svcauth/svcauth.go" {
|
||||
t.Errorf("unerwarteter Verstoß: %+v", violations[0])
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckDiff_PassesWhenTestFileAccompaniesChange(t *testing.T) {
|
||||
changed := []string{"common/svcauth/svcauth.go", "common/svcauth/svcauth_test.go"}
|
||||
|
||||
if v := CheckDiff(changed, testPatterns); len(v) != 0 {
|
||||
t.Fatalf("erwartet keinen Verstoß, bekommen %+v", v)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckDiff_TestInOtherPackageDoesNotCount(t *testing.T) {
|
||||
changed := []string{"common/svcauth/svcauth.go", "common/tenantregistryclient/tenantregistryclient_test.go"}
|
||||
|
||||
if v := CheckDiff(changed, testPatterns); len(v) != 1 {
|
||||
t.Fatalf("erwartet 1 Verstoß, bekommen %+v", v)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckDiff_IgnoresNonGoAndUnrelated(t *testing.T) {
|
||||
changed := []string{"common/svcauth/README.md", "common/httpclient/httpclient.go"}
|
||||
|
||||
if v := CheckDiff(changed, testPatterns); len(v) != 0 {
|
||||
t.Fatalf("erwartet keinen Verstoß, bekommen %+v", v)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckDiff_NoPatternsMeansNoSensitiveAreas(t *testing.T) {
|
||||
if v := CheckDiff([]string{"common/svcauth/svcauth.go"}, nil); len(v) != 0 {
|
||||
t.Fatalf("ohne Muster darf nichts als sensibel gelten, bekommen %+v", v)
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ package testdb
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -16,6 +17,21 @@ import (
|
||||
|
||||
var seq atomic.Int64
|
||||
|
||||
// AdminDSN liefert TEST_ADMIN_DSN. Fehlt er, wird uebersprungen; mit
|
||||
// REQUIRE_DB=1 (CI) zaehlt das Fehlen als Fehler.
|
||||
func AdminDSN(t testing.TB) string {
|
||||
t.Helper()
|
||||
dsn := os.Getenv("TEST_ADMIN_DSN")
|
||||
if dsn != "" {
|
||||
return dsn
|
||||
}
|
||||
if os.Getenv("REQUIRE_DB") == "1" {
|
||||
t.Fatal("testdb: TEST_ADMIN_DSN nicht gesetzt, aber REQUIRE_DB=1")
|
||||
}
|
||||
t.Skip("testdb: TEST_ADMIN_DSN nicht gesetzt, Integrationstest uebersprungen")
|
||||
return ""
|
||||
}
|
||||
|
||||
// Pool legt ein Schema an und liefert einen darauf ausgerichteten Pool;
|
||||
// beides wird per t.Cleanup wieder entfernt.
|
||||
func Pool(t testing.TB, dsn string) *pgxpool.Pool {
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
package testdb
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestAdminDSNGesetzt(t *testing.T) {
|
||||
t.Setenv("TEST_ADMIN_DSN", "postgres://x")
|
||||
t.Setenv("REQUIRE_DB", "1")
|
||||
if got := AdminDSN(t); got != "postgres://x" {
|
||||
t.Fatalf("dsn = %q", got)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user