QA-01: teststrategie-core (pflichttest-gate, testdb-isolationsnachweis, ci-workflow)
This commit is contained in:
@@ -0,0 +1,99 @@
|
||||
package testdbisolation
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// TestParallelProvisioning_AreIsolated ist der automatisierte Nachweis für QA-01 Prüfung 2:
|
||||
// Testdatenbank-Isolation zwischen parallelen Testläufen. Zwei Provisionierungen laufen
|
||||
// gleichzeitig, jede schreibt einen Beleg-Datensatz in ihre eigene DB — keine darf den
|
||||
// Beleg der jeweils anderen sehen.
|
||||
//
|
||||
// Braucht TEST_ADMIN_DSN (siehe Projekt-Testinfrastruktur), läuft auf dem Testhost, nicht lokal
|
||||
// auf der Entwicklungsmaschine (siehe "no installs"-Regel) — wird ohne gesetzte Variable übersprungen.
|
||||
func TestParallelProvisioning_AreIsolated(t *testing.T) {
|
||||
adminDSN := os.Getenv("TEST_ADMIN_DSN")
|
||||
if adminDSN == "" {
|
||||
t.Skip("TEST_ADMIN_DSN nicht gesetzt — Test läuft nur auf dem Testhost, siehe docs/TESTSTRATEGIE-CORE.md")
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
const numParallel = 2
|
||||
dbNames := make([]string, numParallel)
|
||||
for i := range dbNames {
|
||||
dbNames[i] = fmt.Sprintf("qa01_isolation_%d_%d", time.Now().UnixNano(), i)
|
||||
}
|
||||
|
||||
var wg sync.WaitGroup
|
||||
errs := make([]error, numParallel)
|
||||
cleanups := make([]func(context.Context) error, numParallel)
|
||||
|
||||
for i, name := range dbNames {
|
||||
wg.Add(1)
|
||||
go func(idx int, dbName string) {
|
||||
defer wg.Done()
|
||||
|
||||
cleanup, err := ProvisionEphemeralDB(ctx, adminDSN, dbName)
|
||||
if err != nil {
|
||||
errs[idx] = fmt.Errorf("provisionierung %d fehlgeschlagen: %w", idx, err)
|
||||
return
|
||||
}
|
||||
cleanups[idx] = cleanup
|
||||
|
||||
dbDSN := dsnForDB(adminDSN, dbName)
|
||||
// Jede Goroutine schreibt einen für sie eindeutigen Beleg.
|
||||
if err := WriteMarker(ctx, dbDSN, "isolation_marker", fmt.Sprintf("marker-von-goroutine-%d", idx)); err != nil {
|
||||
errs[idx] = fmt.Errorf("markierung %d fehlgeschlagen: %w", idx, err)
|
||||
}
|
||||
}(i, name)
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
t.Cleanup(func() {
|
||||
for _, cleanup := range cleanups {
|
||||
if cleanup != nil {
|
||||
_ = cleanup(context.Background())
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
for i, err := range errs {
|
||||
if err != nil {
|
||||
t.Fatalf("goroutine %d: %v", i, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Isolationsnachweis: jede DB enthält genau ihren eigenen Beleg (1), keinen fremden.
|
||||
for i, name := range dbNames {
|
||||
dbDSN := dsnForDB(adminDSN, name)
|
||||
count, err := CountMarkers(ctx, dbDSN, "isolation_marker")
|
||||
if err != nil {
|
||||
t.Fatalf("zaehlung in db %d fehlgeschlagen: %v", i, err)
|
||||
}
|
||||
if count != 1 {
|
||||
t.Errorf("db %d (%s): erwartet genau 1 eigenen Beleg, gefunden %d — Isolation verletzt", i, name, count)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// dsnForDB ersetzt den Datenbanknamen im Wartungs-DSN durch den gegebenen Namen.
|
||||
func dsnForDB(adminDSN, dbName string) string {
|
||||
idx := strings.LastIndex(adminDSN, "/")
|
||||
if idx == -1 {
|
||||
return adminDSN
|
||||
}
|
||||
base := adminDSN[:idx+1]
|
||||
rest := adminDSN[idx+1:]
|
||||
if q := strings.Index(rest, "?"); q != -1 {
|
||||
return base + dbName + rest[q:]
|
||||
}
|
||||
return base + dbName
|
||||
}
|
||||
Reference in New Issue
Block a user