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