CI / Web-Barrierefreiheitspruefung (axe, Tastatur, Reflow, Textabstaende) (push) Canceled after 0s
CI / Web-Barrierefreiheitspruefung (axe, Tastatur, Reflow, Textabstaende) (pull_request) Canceled after 0s
CI / Core-Modul (Build/Vet/Test) (push) Canceled after 0s
CI / DMS Security-Test-Gate (AC5, Merge-Gate) (push) Canceled after 0s
CI / Connect Security-Test-Gate (QA-01 AC4, Merge-Gate) (push) Canceled after 0s
CI / DMS-Modul Supply-Chain-Scan (govulncheck) (push) Canceled after 0s
CI / Web (dms-browser) (push) Canceled after 0s
CI / Web (pdf-viewer) (push) Canceled after 0s
CI / Web (shl) (push) Canceled after 0s
Supply-Chain-Scan (Go) / govulncheck (push) Canceled after 0s
CI / Core-Modul (Build/Vet/Test) (pull_request) Canceled after 0s
CI / DMS Security-Test-Gate (AC5, Merge-Gate) (pull_request) Canceled after 0s
CI / Connect Security-Test-Gate (QA-01 AC4, Merge-Gate) (pull_request) Canceled after 0s
CI / DMS-Modul Supply-Chain-Scan (govulncheck) (pull_request) Canceled after 0s
CI / Web (dms-browser) (pull_request) Canceled after 0s
CI / Web (pdf-viewer) (pull_request) Canceled after 0s
CI / Web (shl) (pull_request) Canceled after 0s
Core-Schnittstellen-Vertragstests / contract-tests (pull_request) Canceled after 0s
Supply-Chain-Scan (Go) / govulncheck (pull_request) Canceled after 0s
Supply-Chain-Scan (npm) / npm-audit (pull_request) Canceled after 0s
Core-Pflichttest-Gate / pflichttest-gate (pull_request) Canceled after 0s
RBAC-Rollenhierarchie-Pflichtgate / rbac-policy-invariant (pull_request) Canceled after 0s
CI / DMS-Modul (Lint/Build/Test/Coverage/Artefakt) (push) Canceled after 0s
CI / DMS-Modul (Lint/Build/Test/Coverage/Artefakt) (pull_request) Canceled after 0s
Connect-seitige Webhook-Abonnements ueber Core API-07 (zentrale Webhook-Registry): connect/internal/webhookclient als schlanker HTTP-Client gegen die Registry (Subscribe/Unsubscribe), connect/internal/ webhooksub mit lokaler, verschluesselter Abonnementverwaltung und HTTP-Zustellempfaenger inkl. HMAC-Signaturpruefung. Core API-07 exponiert bislang keinen HTTP-Wrapper fuer physisch getrennte Module (anders als z.B. AUD-08) -- Tests laufen deshalb gegen einen den dokumentierten Core-Vertrag nachbildenden Stub, echte Postgres-Testdatenbank, echte HTTP-Verbindungen. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
154 lines
4.5 KiB
Go
154 lines
4.5 KiB
Go
package webhooksub_test
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/jackc/pgx/v5/pgxpool"
|
|
|
|
connectcrypto "gitea.perlbach24.de/scripte/nexarch/connect/internal/crypto"
|
|
"gitea.perlbach24.de/scripte/nexarch/connect/internal/migrate"
|
|
"gitea.perlbach24.de/scripte/nexarch/connect/internal/testdb"
|
|
"gitea.perlbach24.de/scripte/nexarch/connect/internal/webhookclient"
|
|
"gitea.perlbach24.de/scripte/nexarch/connect/internal/webhooksub"
|
|
)
|
|
|
|
type fakeKEK struct{ kek []byte }
|
|
|
|
func (f *fakeKEK) TenantKEK(ctx context.Context, tenantSlug string) ([]byte, error) {
|
|
return f.kek, nil
|
|
}
|
|
|
|
// fakeCoreWebhookAPI reproduziert Core API-07s Subscribe/Unsubscribe-Vertrag
|
|
// -- der von dieser Kachel ausdruecklich erlaubte "minimal gestubte
|
|
// Core-Webhook-Registry-Endpunkt" fuer den echten End-zu-Ende-Test (echte
|
|
// HTTP-Verbindung, echte Datenbank, kein Mock der Client-Logik selbst).
|
|
type fakeCoreWebhookAPI struct {
|
|
mu sync.Mutex
|
|
subscriptions map[string]bool
|
|
secrets map[string]string
|
|
nextID int
|
|
}
|
|
|
|
func newFakeCoreWebhookAPI() *fakeCoreWebhookAPI {
|
|
return &fakeCoreWebhookAPI{subscriptions: make(map[string]bool), secrets: make(map[string]string)}
|
|
}
|
|
|
|
func (f *fakeCoreWebhookAPI) hasSubscription(id string) bool {
|
|
f.mu.Lock()
|
|
defer f.mu.Unlock()
|
|
return f.subscriptions[id]
|
|
}
|
|
|
|
// secretFor liefert das Geheimnis, das beim Subscribe fuer coreSubID
|
|
// uebergeben wurde -- in echt kennt nur Core dieses Geheimnis und signiert
|
|
// damit jede Zustellung; der Test braucht es, um eine echte, gueltige
|
|
// Core-Zustellung nachzubilden.
|
|
func (f *fakeCoreWebhookAPI) secretFor(coreSubID string) string {
|
|
f.mu.Lock()
|
|
defer f.mu.Unlock()
|
|
return f.secrets[coreSubID]
|
|
}
|
|
|
|
func (f *fakeCoreWebhookAPI) handle(w http.ResponseWriter, r *http.Request) {
|
|
if r.Header.Get("X-Service-Token") == "" {
|
|
http.Error(w, "kein service-token", http.StatusUnauthorized)
|
|
return
|
|
}
|
|
switch {
|
|
case r.Method == http.MethodPost && r.URL.Path == "/webhooks/subscriptions":
|
|
var in struct {
|
|
TenantSlug string `json:"tenant_slug"`
|
|
EventType string `json:"event_type"`
|
|
TargetURL string `json:"target_url"`
|
|
Secret string `json:"secret"`
|
|
}
|
|
if err := json.NewDecoder(r.Body).Decode(&in); err != nil {
|
|
http.Error(w, "ungueltiger body", http.StatusBadRequest)
|
|
return
|
|
}
|
|
f.mu.Lock()
|
|
f.nextID++
|
|
id := fmt.Sprintf("core-sub-%d", f.nextID)
|
|
f.subscriptions[id] = true
|
|
f.secrets[id] = in.Secret
|
|
f.mu.Unlock()
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(http.StatusCreated)
|
|
_ = json.NewEncoder(w).Encode(map[string]any{
|
|
"id": id, "event_type": in.EventType, "target_url": in.TargetURL, "created_at": time.Now().UTC(),
|
|
})
|
|
case r.Method == http.MethodDelete:
|
|
id := strings.TrimPrefix(r.URL.Path, "/webhooks/subscriptions/")
|
|
f.mu.Lock()
|
|
_, existed := f.subscriptions[id]
|
|
delete(f.subscriptions, id)
|
|
f.mu.Unlock()
|
|
if !existed {
|
|
http.Error(w, "unbekanntes abonnement", http.StatusNotFound)
|
|
return
|
|
}
|
|
w.WriteHeader(http.StatusNoContent)
|
|
default:
|
|
http.Error(w, "nicht unterstuetzt", http.StatusNotFound)
|
|
}
|
|
}
|
|
|
|
// testEnv buendelt einen Manager gegen eine echte, isolierte Postgres-
|
|
// Testdatenbank und einen echten (gestubten) Core-Webhook-Server.
|
|
type testEnv struct {
|
|
Manager *webhooksub.Manager
|
|
Pool *pgxpool.Pool
|
|
Core *fakeCoreWebhookAPI
|
|
}
|
|
|
|
func setup(t *testing.T) testEnv {
|
|
t.Helper()
|
|
dsn := os.Getenv("TEST_TENANT_DSN")
|
|
if dsn == "" {
|
|
t.Skip("TEST_TENANT_DSN nicht gesetzt, Integrationstest uebersprungen")
|
|
}
|
|
ctx := context.Background()
|
|
pool := testdb.Pool(t, dsn)
|
|
|
|
migrations, err := migrate.Load(findMigrationsDir(t))
|
|
if err != nil {
|
|
t.Fatalf("migrationen laden: %v", err)
|
|
}
|
|
if _, err := migrate.Up(ctx, pool, migrations); err != nil {
|
|
t.Fatalf("migrationen anwenden: %v", err)
|
|
}
|
|
|
|
core := newFakeCoreWebhookAPI()
|
|
coreSrv := httptest.NewServer(http.HandlerFunc(core.handle))
|
|
t.Cleanup(coreSrv.Close)
|
|
|
|
cryptoSvc := connectcrypto.NewService(&fakeKEK{kek: make([]byte, connectcrypto.KEKSize)})
|
|
client := webhookclient.New(coreSrv.URL, "svc-token-test")
|
|
mgr := webhooksub.NewManager(pool, cryptoSvc, client, "https://connect.example.test/connector-webhooks/deliveries")
|
|
|
|
return testEnv{Manager: mgr, Pool: pool, Core: core}
|
|
}
|
|
|
|
func findMigrationsDir(t *testing.T) string {
|
|
t.Helper()
|
|
dir, _ := os.Getwd()
|
|
for i := 0; i < 6; i++ {
|
|
p := dir + "/migrations"
|
|
if fi, err := os.Stat(p); err == nil && fi.IsDir() {
|
|
return p
|
|
}
|
|
dir += "/.."
|
|
}
|
|
t.Fatal("connect/migrations nicht gefunden")
|
|
return ""
|
|
}
|