QA-09: Vertragstest fuer API-07-Wiederholung nach zeitweisem Ausfall
Ergaenzt internal/contracttest (Core QA-07) um den fuer DMS QA-09 Pruefung 3 noetigen Nachweis: ein Webhook-Ziel, das nach mehreren Fehlversuchen wieder erreichbar ist, wird trotzdem noch zugestellt (TestProcessDue_RetriesWithBackoffThenMarksFailed in internal/webhook/webhook_test.go belegt bereits den dauerhaften Fehlschlag, nicht aber die Wiederherstellung). Liegt im Core-Modul, da DMS die Core-internen Pakete (Dispatcher/Store) nicht importieren kann (getrenntes Go-Modul, siehe dms/internal/retentionclient-Muster). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
abc7e4b3ed
commit
a76350a534
1 file changed
+102
@@ -0,0 +1,102 @@
|
||||
package contracttest
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"gitea.perlbach24.de/scripte/nexarch/internal/testdb"
|
||||
"gitea.perlbach24.de/scripte/nexarch/internal/webhook"
|
||||
)
|
||||
|
||||
// DMS QA-09 Pruefung 3: Wiederholungsverhalten bei Ausfall ist belegt —
|
||||
// Retry-Logik aus Core API-07 greift nachweislich gegen einen absichtlich
|
||||
// zeitweise nicht erreichbaren Webhook-Empfaenger. internal/webhook/
|
||||
// webhook_test.go belegt bereits den PERMANENTEN Fehlschlag nach
|
||||
// MaxAttempts (TestProcessDue_RetriesWithBackoffThenMarksFailed); dieser
|
||||
// Test ergaenzt den fuer QA-09 noch fehlenden Fall, den ein konsumierendes
|
||||
// Modul wie DMS tatsaechlich erwartet: ein Ziel, das nach wenigen
|
||||
// Fehlversuchen wieder erreichbar ist, wird trotzdem noch zugestellt.
|
||||
var webhookTestSecretKey = []byte("0123456789abcdef0123456789abcdef"[:32])
|
||||
|
||||
func TestContract_API07_RetryRecoversAfterTransientFailures(t *testing.T) {
|
||||
adminDSN := os.Getenv("TEST_ADMIN_DSN")
|
||||
if adminDSN == "" {
|
||||
t.Skip("TEST_ADMIN_DSN nicht gesetzt, Vertragstest uebersprungen")
|
||||
}
|
||||
ctx := context.Background()
|
||||
pool := testdb.Pool(t, adminDSN)
|
||||
if _, err := pool.Exec(ctx, `
|
||||
CREATE TABLE IF NOT EXISTS webhook_subscriptions (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(), event_type TEXT NOT NULL,
|
||||
target_url TEXT NOT NULL, secret_ciphertext BYTEA NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS webhook_deliveries (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(), subscription_id UUID NOT NULL REFERENCES webhook_subscriptions(id),
|
||||
event_type TEXT NOT NULL, payload JSONB NOT NULL, status TEXT NOT NULL DEFAULT 'pending',
|
||||
attempt INT NOT NULL DEFAULT 0, next_attempt_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
last_error TEXT, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), delivered_at TIMESTAMPTZ
|
||||
);
|
||||
`); err != nil {
|
||||
t.Fatalf("schema: %v", err)
|
||||
}
|
||||
store, err := webhook.NewStore(pool, webhookTestSecretKey)
|
||||
if err != nil {
|
||||
t.Fatalf("newstore: %v", err)
|
||||
}
|
||||
|
||||
const failuresBeforeSuccess = 2
|
||||
var attempts int32
|
||||
target := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
n := atomic.AddInt32(&attempts, 1)
|
||||
if n <= failuresBeforeSuccess {
|
||||
w.WriteHeader(http.StatusServiceUnavailable)
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}))
|
||||
defer target.Close()
|
||||
|
||||
loopbackPolicy := webhook.TargetPolicy{AllowHTTP: true, Allow: []string{"127.0.0.1"}}
|
||||
eventType := "dms.qa09.retry-" + time.Now().Format("150405.000000000")
|
||||
sub, err := store.Subscribe(ctx, loopbackPolicy, eventType, target.URL, "qa09-secret")
|
||||
if err != nil {
|
||||
t.Fatalf("subscribe: %v", err)
|
||||
}
|
||||
if _, err := store.Enqueue(ctx, eventType, map[string]string{"document_id": "doc-qa09"}); err != nil {
|
||||
t.Fatalf("enqueue: %v", err)
|
||||
}
|
||||
|
||||
dispatcher, err := webhook.NewDispatcher(pool, webhookTestSecretKey, target.Client())
|
||||
if err != nil {
|
||||
t.Fatalf("newdispatcher: %v", err)
|
||||
}
|
||||
dispatcher.MaxAttempts = failuresBeforeSuccess + 3
|
||||
dispatcher.BaseBackoff = 30 * time.Millisecond
|
||||
|
||||
deadline := time.Now().Add(5 * time.Second)
|
||||
var status string
|
||||
for time.Now().Before(deadline) {
|
||||
if _, err := dispatcher.ProcessDue(ctx); err != nil {
|
||||
t.Fatalf("processdue: %v", err)
|
||||
}
|
||||
if err := pool.QueryRow(ctx, `SELECT status FROM webhook_deliveries WHERE subscription_id = $1`, sub.ID).Scan(&status); err != nil {
|
||||
t.Fatalf("status lesen: %v", err)
|
||||
}
|
||||
if status == webhook.StatusDelivered || status == webhook.StatusFailed {
|
||||
break
|
||||
}
|
||||
time.Sleep(20 * time.Millisecond)
|
||||
}
|
||||
|
||||
if status != webhook.StatusDelivered {
|
||||
t.Fatalf("status nach wiederholten versuchen = %q, want %q (Retry-Logik hat ein wieder erreichbares Ziel nicht zugestellt)", status, webhook.StatusDelivered)
|
||||
}
|
||||
if got := atomic.LoadInt32(&attempts); got <= failuresBeforeSuccess {
|
||||
t.Fatalf("tatsaechliche zustellversuche = %d, erwartet mehr als %d (kein echter Retry stattgefunden)", got, failuresBeforeSuccess)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user