Files
patrickandClaude Sonnet 5 0fef62b2a3
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
API-03: 03 externe webhook abonnements nutzt core api 07
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>
2026-10-16 13:18:10 +00:00

110 lines
3.7 KiB
Go

package webhookclient
import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"time"
)
// newFakeCoreWebhookAPI steht fuer Core API-07s echten Vertrag (POST
// /webhooks/subscriptions, DELETE /webhooks/subscriptions/{id}) -- testet
// die echte HTTP-Kette (Client -> echter Server -> Antwort-Dekodierung),
// nicht nur eine wirkungslose lokale Struktur.
func newFakeCoreWebhookAPI(t *testing.T) (*httptest.Server, *[]string) {
t.Helper()
var calls []string
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
calls = append(calls, r.Method+" "+r.URL.String())
if r.Header.Get("X-Service-Token") == "" {
t.Fatalf("Service-Token fehlt bei %s %s", r.Method, r.URL.Path)
}
switch {
case r.Method == http.MethodPost && r.URL.Path == "/webhooks/subscriptions":
var in subscribeRequest
if err := json.NewDecoder(r.Body).Decode(&in); err != nil {
t.Fatalf("body dekodieren: %v", err)
}
w.WriteHeader(http.StatusCreated)
_ = json.NewEncoder(w).Encode(Subscription{
ID: "core-sub-1", EventType: in.EventType, TargetURL: in.TargetURL, CreatedAt: time.Now().UTC(),
})
case r.Method == http.MethodDelete:
w.WriteHeader(http.StatusNoContent)
default:
w.WriteHeader(http.StatusNotFound)
}
}))
return srv, &calls
}
func TestSubscribe_SendsRequestFields_ReturnsSubscription(t *testing.T) {
srv, _ := newFakeCoreWebhookAPI(t)
defer srv.Close()
c := New(srv.URL, "svc-token-123")
sub, err := c.Subscribe(context.Background(), "acme", "dms.document.archived", "https://connect.example/d/abc", "shh-secret")
if err != nil {
t.Fatalf("Subscribe: %v", err)
}
if sub.ID != "core-sub-1" {
t.Fatalf("erwartete core-vergebene id, bekam %q", sub.ID)
}
if sub.EventType != "dms.document.archived" {
t.Fatalf("event_type nicht wie gesendet: %q", sub.EventType)
}
}
func TestSubscribe_UnexpectedStatus_ReturnsError(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusInternalServerError)
}))
defer srv.Close()
c := New(srv.URL, "svc-token-123")
if _, err := c.Subscribe(context.Background(), "acme", "dms.document.archived", "https://connect.example/d/abc", "shh"); err == nil {
t.Fatal("erwarteter Fehler bei Status 500 blieb aus")
}
}
func TestSubscribe_TransportError_ReturnsError(t *testing.T) {
c := New("http://127.0.0.1:1", "svc-token-123")
if _, err := c.Subscribe(context.Background(), "acme", "dms.document.archived", "https://connect.example/d/abc", "shh"); err == nil {
t.Fatal("erwarteter Transport-Fehler blieb aus")
}
}
func TestUnsubscribe_SendsServiceTokenAndTenant_Succeeds(t *testing.T) {
srv, calls := newFakeCoreWebhookAPI(t)
defer srv.Close()
c := New(srv.URL, "svc-token-123")
if err := c.Unsubscribe(context.Background(), "acme", "core-sub-1"); err != nil {
t.Fatalf("Unsubscribe: %v", err)
}
if len(*calls) != 1 || (*calls)[0] != "DELETE /webhooks/subscriptions/core-sub-1?tenant_slug=acme" {
t.Fatalf("unerwartete aufrufe: %v", *calls)
}
}
func TestUnsubscribe_UnexpectedStatus_ReturnsError(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusInternalServerError)
}))
defer srv.Close()
c := New(srv.URL, "svc-token-123")
if err := c.Unsubscribe(context.Background(), "acme", "core-sub-1"); err == nil {
t.Fatal("erwarteter Fehler bei Status 500 blieb aus")
}
}
func TestUnsubscribe_TransportError_ReturnsError(t *testing.T) {
c := New("http://127.0.0.1:1", "svc-token-123")
if err := c.Unsubscribe(context.Background(), "acme", "core-sub-1"); err == nil {
t.Fatal("erwarteter Transport-Fehler blieb aus")
}
}