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

158 lines
6.0 KiB
Go

package webhooksub_test
import (
"context"
"errors"
"net/http"
"net/http/httptest"
"testing"
connectcrypto "gitea.perlbach24.de/scripte/nexarch/connect/internal/crypto"
"gitea.perlbach24.de/scripte/nexarch/connect/internal/webhookclient"
"gitea.perlbach24.de/scripte/nexarch/connect/internal/webhooksub"
)
// Pruefung 1 / Akzeptanzkriterium 1: Testabonnement wird ueber die
// (gestubte) Core-Webhook-Registry registriert.
func TestSubscribe_RegistersAtCoreAndPersistsLocally(t *testing.T) {
env := setup(t)
env.Manager.RegisterAction("testcrm", "dms.document.archived", func(context.Context, []byte) error { return nil })
reg, err := env.Manager.Subscribe(context.Background(), "acme", "testcrm", "dms.document.archived")
if err != nil {
t.Fatalf("Subscribe: %v", err)
}
if reg.ID == "" {
t.Fatal("erwartete lokale id")
}
var count int
var coreSubID string
if err := env.Pool.QueryRow(context.Background(), `
SELECT count(*), max(core_subscription_id) FROM connector_webhook_subscriptions WHERE id = $1
`, reg.ID).Scan(&count, &coreSubID); err != nil {
t.Fatalf("row lesen: %v", err)
}
if count != 1 {
t.Fatalf("erwartete genau eine zeile, habe %d", count)
}
if !env.Core.hasSubscription(coreSubID) {
t.Fatalf("core-abonnement %q nicht bei core registriert", coreSubID)
}
}
func TestSubscribe_WithoutRegisteredAction_Fails(t *testing.T) {
env := setup(t)
_, err := env.Manager.Subscribe(context.Background(), "acme", "testcrm", "dms.document.archived")
if !errors.Is(err, webhooksub.ErrActionNotRegistered) {
t.Fatalf("erwartete ErrActionNotRegistered, bekam %v", err)
}
}
// Negativtest: schlaegt die Core-Registrierung fehl, bleibt lokal nichts
// zurueck (kein verwaistes Abonnement ohne Core-Gegenstueck).
func TestSubscribe_CoreFailure_PersistsNothingLocally(t *testing.T) {
env := setup(t)
env.Manager.RegisterAction("testcrm", "dms.document.archived", func(context.Context, []byte) error { return nil })
failingCore := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusInternalServerError)
}))
defer failingCore.Close()
cryptoSvc := connectcrypto.NewService(&fakeKEK{kek: make([]byte, connectcrypto.KEKSize)})
mgr := webhooksub.NewManager(env.Pool, cryptoSvc, webhookclient.New(failingCore.URL, "svc-token-test"), "https://connect.example.test/connector-webhooks/deliveries")
mgr.RegisterAction("testcrm", "dms.document.archived", func(context.Context, []byte) error { return nil })
if _, err := mgr.Subscribe(context.Background(), "acme", "testcrm", "dms.document.archived"); err == nil {
t.Fatal("erwarteter fehler bei core-ausfall blieb aus")
}
var count int
if err := env.Pool.QueryRow(context.Background(), `
SELECT count(*) FROM connector_webhook_subscriptions WHERE tenant_slug = 'acme' AND connector_name = 'testcrm'
`).Scan(&count); err != nil {
t.Fatalf("zeilen zaehlen: %v", err)
}
if count != 0 {
t.Fatalf("erwartete keine lokale zeile nach core-ausfall, habe %d", count)
}
}
// Pruefung 3 / Akzeptanzkriterium 3: Abbestellen entfernt das Abonnement
// vollstaendig -- bei Core und lokal.
func TestUnsubscribe_RemovesAtCoreAndLocally_Completely(t *testing.T) {
env := setup(t)
env.Manager.RegisterAction("testcrm", "dms.document.archived", func(context.Context, []byte) error { return nil })
reg, err := env.Manager.Subscribe(context.Background(), "acme", "testcrm", "dms.document.archived")
if err != nil {
t.Fatalf("Subscribe: %v", err)
}
var coreSubID string
if err := env.Pool.QueryRow(context.Background(), `
SELECT core_subscription_id FROM connector_webhook_subscriptions WHERE id = $1
`, reg.ID).Scan(&coreSubID); err != nil {
t.Fatalf("core_subscription_id lesen: %v", err)
}
if err := env.Manager.Unsubscribe(context.Background(), "acme", "testcrm", "dms.document.archived"); err != nil {
t.Fatalf("Unsubscribe: %v", err)
}
var count int
if err := env.Pool.QueryRow(context.Background(), `
SELECT count(*) FROM connector_webhook_subscriptions WHERE id = $1
`, reg.ID).Scan(&count); err != nil {
t.Fatalf("zeilen zaehlen: %v", err)
}
if count != 0 {
t.Fatalf("erwartete vollstaendig entfernte lokale zeile, habe noch %d", count)
}
if env.Core.hasSubscription(coreSubID) {
t.Fatal("erwartete bei core entferntes abonnement, ist noch vorhanden")
}
}
func TestUnsubscribe_UnknownSubscription_ReturnsNotFound(t *testing.T) {
env := setup(t)
err := env.Manager.Unsubscribe(context.Background(), "acme", "testcrm", "dms.document.archived")
if !errors.Is(err, webhooksub.ErrNotFound) {
t.Fatalf("erwartete ErrNotFound, bekam %v", err)
}
}
// Negativtest: schlaegt die Core-Abbestellung fehl, bleibt die lokale Zeile
// fuer einen Wiederholungsversuch bestehen statt ein verwaistes
// Core-Abonnement zu hinterlassen.
func TestUnsubscribe_CoreFailure_KeepsLocalRowForRetry(t *testing.T) {
env := setup(t)
env.Manager.RegisterAction("testcrm", "dms.document.archived", func(context.Context, []byte) error { return nil })
reg, err := env.Manager.Subscribe(context.Background(), "acme", "testcrm", "dms.document.archived")
if err != nil {
t.Fatalf("Subscribe: %v", err)
}
brokenCore := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusInternalServerError)
}))
defer brokenCore.Close()
cryptoSvc := connectcrypto.NewService(&fakeKEK{kek: make([]byte, connectcrypto.KEKSize)})
mgrWithBrokenCore := webhooksub.NewManager(env.Pool, cryptoSvc, webhookclient.New(brokenCore.URL, "svc-token-test"), "https://connect.example.test/connector-webhooks/deliveries")
if err := mgrWithBrokenCore.Unsubscribe(context.Background(), "acme", "testcrm", "dms.document.archived"); err == nil {
t.Fatal("erwarteter fehler bei core-ausfall blieb aus")
}
var count int
if err := env.Pool.QueryRow(context.Background(), `
SELECT count(*) FROM connector_webhook_subscriptions WHERE id = $1
`, reg.ID).Scan(&count); err != nil {
t.Fatalf("zeilen zaehlen: %v", err)
}
if count != 1 {
t.Fatalf("erwartete erhaltene lokale zeile nach core-ausfall, habe %d", count)
}
}