Files
patrick 1afffe5028
CI / Web-Barrierefreiheitspruefung (axe, Tastatur, Reflow, Textabstaende) (push) 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 / 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
CI / DMS-Modul (Lint/Build/Test/Coverage/Artefakt) (push) Canceled after 0s
INT-20: fix created_by fk mismatch and test vault stubs
2026-09-22 12:55:05 +02:00

158 lines
5.3 KiB
Go

package triggerapi
import (
"bytes"
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"strings"
"testing"
"gitea.perlbach24.de/scripte/nexarch/dms/internal/tag"
"gitea.perlbach24.de/scripte/nexarch/dms/internal/testenv"
"gitea.perlbach24.de/scripte/nexarch/dms/internal/trigger"
)
// memVault verpackt reversibel (Praefix) statt echt zu verschluesseln, damit
// der Test einen echten Seal/Open-Roundtrip pruefen kann, ohne einen
// KEK-Provider hochzufahren.
type memVault struct{}
var _ trigger.Vault = memVault{}
func (memVault) Seal(_ context.Context, _, plaintext string) ([]byte, []byte, error) {
return []byte("dek"), []byte("enc:" + plaintext), nil
}
func (memVault) Open(_ context.Context, _ string, _, ciphertext []byte) (string, error) {
return strings.TrimPrefix(string(ciphertext), "enc:"), nil
}
func TestAPI_ManageTriggersOverHTTP(t *testing.T) {
e := testenv.New(t)
tg, err := tag.Create(t.Context(), e.Pool, "rechnung", "", nil)
if err != nil {
t.Fatal(err)
}
mux := http.NewServeMux()
NewServer(e.Pool, memVault{}, testenv.TenantSlug).RegisterRoutes(mux)
srv := httptest.NewServer(mux)
t.Cleanup(srv.Close)
do := func(method, path string, body any) (int, []byte) {
var r *http.Request
var err error
if body != nil {
b, _ := json.Marshal(body)
r, err = http.NewRequest(method, srv.URL+path, bytes.NewReader(b))
} else {
r, err = http.NewRequest(method, srv.URL+path, nil)
}
if err != nil {
t.Fatal(err)
}
resp, err := http.DefaultClient.Do(r)
if err != nil {
t.Fatal(err)
}
defer func() { _ = resp.Body.Close() }()
var buf bytes.Buffer
_, _ = buf.ReadFrom(resp.Body)
return resp.StatusCode, buf.Bytes()
}
caller := "?caller=" + e.UserID
trg := map[string]any{"name": "Rechnung", "event": "document.uploaded", "filename_pattern": "(?i)rechnung", "action_type": "set_tag", "action_tag_id": tg.ID}
if status, _ := do(http.MethodPost, "/triggers", trg); status != http.StatusBadRequest {
t.Errorf("ohne caller: %d, want 400", status)
}
status, body := do(http.MethodPost, "/triggers"+caller, trg)
if status != http.StatusCreated {
t.Fatalf("anlegen: %d %s", status, body)
}
var created map[string]any
_ = json.Unmarshal(body, &created)
id := created["id"].(string)
if created["enabled"] != true {
t.Errorf("neuer trigger sollte aktiviert sein: %+v", created)
}
bad := map[string]any{"name": "kaputt", "event": "document.uploaded", "filename_pattern": "([", "action_type": "set_tag", "action_tag_id": tg.ID}
if status, _ := do(http.MethodPost, "/triggers"+caller, bad); status != http.StatusBadRequest {
t.Errorf("ungueltiges muster: %d, want 400", status)
}
status, body = do(http.MethodGet, "/triggers", nil)
var list []map[string]any
_ = json.Unmarshal(body, &list)
if status != http.StatusOK || len(list) != 1 {
t.Errorf("liste: %d %s", status, body)
}
if _, hasSecret := list[0]["webhook_secret"]; hasSecret {
t.Error("die liste darf kein geheimnis enthalten")
}
if status, _ := do(http.MethodGet, "/triggers/"+id, nil); status != http.StatusOK {
t.Errorf("lesen: %d", status)
}
if status, _ := do(http.MethodGet, "/triggers/00000000-0000-0000-0000-000000000001", nil); status != http.StatusNotFound {
t.Errorf("unbekannt: %d, want 404", status)
}
if status, _ := do(http.MethodPost, "/triggers/"+id+"/disable"+caller, nil); status != http.StatusNoContent {
t.Errorf("deaktivieren: %d", status)
}
_, body = do(http.MethodGet, "/triggers/"+id, nil)
var after map[string]any
_ = json.Unmarshal(body, &after)
if after["enabled"] != false {
t.Errorf("trigger sollte deaktiviert sein: %+v", after)
}
if status, _ := do(http.MethodPost, "/triggers/"+id+"/enable"+caller, nil); status != http.StatusNoContent {
t.Errorf("aktivieren: %d", status)
}
if status, _ := do(http.MethodDelete, "/triggers/"+id+caller, nil); status != http.StatusNoContent {
t.Errorf("loeschen: %d", status)
}
if status, _ := do(http.MethodGet, "/triggers/"+id, nil); status != http.StatusNotFound {
t.Errorf("nach loeschen: %d, want 404", status)
}
if status, _ := do(http.MethodDelete, "/triggers/"+id+caller, nil); status != http.StatusNotFound {
t.Errorf("doppelt loeschen: %d, want 404", status)
}
}
func TestAPI_WebhookSecretNeverReturned(t *testing.T) {
e := testenv.New(t)
mux := http.NewServeMux()
NewServer(e.Pool, memVault{}, testenv.TenantSlug).RegisterRoutes(mux)
srv := httptest.NewServer(mux)
t.Cleanup(srv.Close)
url := "https://example.test/hook"
trg := map[string]any{"name": "Hook", "event": "document.uploaded", "action_type": "webhook", "webhook_url": url, "webhook_secret": "top-secret"}
b, _ := json.Marshal(trg)
resp, err := http.Post(srv.URL+"/triggers?caller="+e.UserID, "application/json", bytes.NewReader(b))
if err != nil {
t.Fatal(err)
}
defer func() { _ = resp.Body.Close() }()
var out bytes.Buffer
_, _ = out.ReadFrom(resp.Body)
if bytes.Contains(out.Bytes(), []byte("top-secret")) {
t.Fatalf("antwort enthaelt das geheimnis im klartext: %s", out.String())
}
var wrapped, enc []byte
if err := e.Pool.QueryRow(t.Context(), `SELECT webhook_secret_wrapped_dek, webhook_secret_enc FROM triggers WHERE webhook_url = $1`, url).Scan(&wrapped, &enc); err != nil {
t.Fatal(err)
}
if bytes.Equal(enc, []byte("top-secret")) {
t.Fatal("geheimnis liegt unverschluesselt in der datenbank")
}
if len(wrapped) == 0 {
t.Fatal("kein verpackter schluessel gespeichert")
}
}