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>
158 lines
6.0 KiB
Go
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)
|
|
}
|
|
}
|