Files
nexarch/workflow/internal/routingruleapi/handler_test.go
T
patrickandClaude Sonnet 5 76b736a3df
CI / Web-Barrierefreiheitspruefung (axe, Tastatur, Reflow, Textabstaende) (pull_request) Canceled after 0s
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) (pull_request) Canceled after 0s
CI / Web (dms-browser) (pull_request) Canceled after 0s
Core-Pflichttest-Gate / pflichttest-gate (pull_request) Canceled after 0s
Supply-Chain-Scan (npm) / npm-audit (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 / 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
RBAC-Rollenhierarchie-Pflichtgate / rbac-policy-invariant (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
CI / DMS-Modul (Lint/Build/Test/Coverage/Artefakt) (pull_request) Canceled after 0s
CI / DMS-Modul (Lint/Build/Test/Coverage/Artefakt) (push) Canceled after 0s
RTE-03: routing regel editor oberflaeche
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-26 12:51:10 +02:00

185 lines
5.9 KiB
Go

package routingruleapi
import (
"bytes"
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"testing"
"gitea.perlbach24.de/scripte/nexarch/workflow/internal/migrate"
"gitea.perlbach24.de/scripte/nexarch/workflow/internal/routingrule"
"gitea.perlbach24.de/scripte/nexarch/workflow/internal/testdb"
)
func findModuleRoot(t *testing.T) string {
t.Helper()
dir, err := os.Getwd()
if err != nil {
t.Fatalf("getwd: %v", err)
}
for {
if _, err := os.Stat(filepath.Join(dir, "go.mod")); err == nil {
return dir
}
parent := filepath.Dir(dir)
if parent == dir {
t.Fatal("go.mod nicht gefunden")
}
dir = parent
}
}
func newTestServer(t *testing.T) *Server {
t.Helper()
dsn := os.Getenv("TEST_TENANT_DSN")
if dsn == "" {
t.Skip("TEST_TENANT_DSN nicht gesetzt, Integrationstest uebersprungen")
}
ctx := context.Background()
pool := testdb.Pool(t, dsn)
migrations, err := migrate.Load(filepath.Join(findModuleRoot(t), "migrations", "tenant"))
if err != nil {
t.Fatalf("migrationen laden: %v", err)
}
if _, err := migrate.Up(ctx, pool, migrations); err != nil {
t.Fatalf("migrationen anwenden: %v", err)
}
return NewServer(routingrule.NewStore(pool))
}
func doJSON(t *testing.T, s *Server, method, path string, body any, userID string) *httptest.ResponseRecorder {
t.Helper()
var buf bytes.Buffer
if body != nil {
if err := json.NewEncoder(&buf).Encode(body); err != nil {
t.Fatalf("body kodieren: %v", err)
}
}
req := httptest.NewRequest(method, path, &buf)
if userID != "" {
req.Header.Set("X-User-Id", userID)
}
mux := http.NewServeMux()
s.RegisterRoutes(mux)
rec := httptest.NewRecorder()
mux.ServeHTTP(rec, req)
return rec
}
func TestHandleList_RequiresUser(t *testing.T) {
s := newTestServer(t)
rec := doJSON(t, s, "GET", "/routing-rules", nil, "")
if rec.Code != http.StatusUnauthorized {
t.Fatalf("erwartet 401, bekommen %d", rec.Code)
}
}
// AC1: Regeln sind priorisierbar (SetPriority ueber die API erreichbar).
func TestHandleSetPriority_ReordersRule(t *testing.T) {
s := newTestServer(t)
rec := doJSON(t, s, "POST", "/routing-rules", ruleInput{
Name: "regel-a", Priority: 1,
Conditions: []routingrule.Condition{{Field: "object_type", Operator: "equals", Value: "invoice"}},
Actions: []routingrule.Action{{Type: "route_to_process", Target: "approval"}},
}, "u1")
if rec.Code != http.StatusCreated {
t.Fatalf("anlegen fehlgeschlagen: %d %s", rec.Code, rec.Body.String())
}
var created routingrule.Rule
if err := json.Unmarshal(rec.Body.Bytes(), &created); err != nil {
t.Fatalf("antwort dekodieren: %v", err)
}
rec = doJSON(t, s, "PUT", "/routing-rules/"+created.ID+"/priority", priorityInput{Priority: 99}, "u1")
if rec.Code != http.StatusNoContent {
t.Fatalf("prioritaet setzen fehlgeschlagen: %d %s", rec.Code, rec.Body.String())
}
rec = doJSON(t, s, "GET", "/routing-rules", nil, "u1")
var rules []routingrule.Rule
if err := json.Unmarshal(rec.Body.Bytes(), &rules); err != nil {
t.Fatalf("liste dekodieren: %v", err)
}
if len(rules) == 0 || rules[0].ID != created.ID || rules[0].Priority != 99 {
t.Fatalf("regel nicht mit neuer prioritaet an erster stelle: %+v", rules)
}
}
// AC3 / Pruefung 2: Testlauf einer Regel vor Aktivierung, ohne echte
// Ausfuehrung anzustossen (keine routing_decisions-Zeile).
func TestHandleTest_MatchesEnabledRuleByPriority(t *testing.T) {
s := newTestServer(t)
doJSON(t, s, "POST", "/routing-rules", ruleInput{
Name: "niedrig", Priority: 1,
Conditions: []routingrule.Condition{{Field: "object_type", Operator: "equals", Value: "invoice"}},
Actions: []routingrule.Action{{Type: "route_to_process", Target: "low"}},
}, "u1")
doJSON(t, s, "POST", "/routing-rules", ruleInput{
Name: "hoch", Priority: 10,
Conditions: []routingrule.Condition{{Field: "object_type", Operator: "equals", Value: "invoice"}},
Actions: []routingrule.Action{{Type: "route_to_process", Target: "high"}},
}, "u1")
rec := doJSON(t, s, "POST", "/routing-rules/test", testInput{Object: map[string]any{"object_type": "invoice"}}, "u1")
if rec.Code != http.StatusOK {
t.Fatalf("testlauf fehlgeschlagen: %d %s", rec.Code, rec.Body.String())
}
var result testResult
if err := json.Unmarshal(rec.Body.Bytes(), &result); err != nil {
t.Fatalf("antwort dekodieren: %v", err)
}
if !result.Matched || result.RuleName != "hoch" {
t.Fatalf("erwartet match auf hoehere prioritaet, bekommen %+v", result)
}
}
func TestHandleTest_NoMatch(t *testing.T) {
s := newTestServer(t)
rec := doJSON(t, s, "POST", "/routing-rules/test", testInput{Object: map[string]any{"object_type": "unbekannt"}}, "u1")
if rec.Code != http.StatusOK {
t.Fatalf("testlauf fehlgeschlagen: %d %s", rec.Code, rec.Body.String())
}
var result testResult
if err := json.Unmarshal(rec.Body.Bytes(), &result); err != nil {
t.Fatalf("antwort dekodieren: %v", err)
}
if result.Matched {
t.Fatalf("erwartet kein match, bekommen %+v", result)
}
}
// Pruefung 3: ungueltige Bedingungseingabe wird abgewiesen.
func TestHandleCreate_RejectsInvalidCondition(t *testing.T) {
s := newTestServer(t)
rec := doJSON(t, s, "POST", "/routing-rules", ruleInput{
Name: "ungueltig",
Priority: 1,
Conditions: []routingrule.Condition{
{Field: "object_type", Operator: "nicht_unterstuetzt", Value: "invoice"},
},
Actions: []routingrule.Action{{Type: "route_to_process", Target: "x"}},
}, "u1")
if rec.Code != http.StatusBadRequest {
t.Fatalf("erwartet 400 bei ungueltigem operator, bekommen %d", rec.Code)
}
}
func TestHandleCreate_RejectsMissingField(t *testing.T) {
s := newTestServer(t)
rec := doJSON(t, s, "POST", "/routing-rules", ruleInput{
Name: "ungueltig-2",
Priority: 1,
Conditions: []routingrule.Condition{
{Field: "", Operator: "equals", Value: "invoice"},
},
Actions: []routingrule.Action{{Type: "route_to_process", Target: "x"}},
}, "u1")
if rec.Code != http.StatusBadRequest {
t.Fatalf("erwartet 400 bei fehlendem feld, bekommen %d", rec.Code)
}
}