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) Successful in 38s
CI / DMS Security-Test-Gate (AC5, Merge-Gate) (push) Successful in 3s
CI / Connect Security-Test-Gate (QA-01 AC4, Merge-Gate) (push) Successful in 3s
CI / DMS-Modul Supply-Chain-Scan (govulncheck) (push) Failing after 14s
CI / Web (dms-browser) (push) Failing after 53s
CI / Web (pdf-viewer) (push) Failing after 38s
CI / Web (shl) (push) Failing after 8s
Supply-Chain-Scan (Go) / govulncheck (push) Failing after 37s
AI-Pflichttest-Gate / pflichttest-gate (pull_request) Successful in 15m54s
CI / Core-Modul (Build/Vet/Test) (pull_request) Successful in 37s
CI / DMS Security-Test-Gate (AC5, Merge-Gate) (pull_request) Successful in 2s
CI / Connect Security-Test-Gate (QA-01 AC4, Merge-Gate) (pull_request) Successful in 2s
CI / DMS-Modul Supply-Chain-Scan (govulncheck) (pull_request) Failing after 13s
CI / Web (dms-browser) (pull_request) Failing after 53s
CI / Web (pdf-viewer) (pull_request) Failing after 38s
CI / Web (shl) (pull_request) Failing after 8s
Core-Schnittstellen-Vertragstests / contract-tests (pull_request) Successful in 16m2s
Supply-Chain-Scan (Go) / govulncheck (pull_request) Failing after 56s
Supply-Chain-Scan (npm) / npm-audit (pull_request) Failing after 1m13s
Core-Pflichttest-Gate / pflichttest-gate (pull_request) Successful in 16m18s
RBAC-Rollenhierarchie-Pflichtgate / rbac-policy-invariant (pull_request) Successful in 15m49s
Workflow-Pflichttest-Gate / pflichttest-gate (pull_request) Failing after 22m6s
DB-Integrationstests / db-tests (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
- semanticsearchapi: ai.search.read per policyclient (WithPolicy), Tenant aus
dem Anfragekoerper, nach Service-Token-Pruefung
- taskapi: workflow.task.read (GET /tasks) und workflow.task.write (PATCH
status); Paket jetzt in cmd/workflow-app verdrahtet (lag bisher nur im
a11y-Fixture) und damit hinter bearerauth
- connectorsetupapi: connect.setup.read/write je Route
- Migration 0029 (up/down): neue Rechte fuer user und tenant_admin
- Wiring: NEXARCH_{AI,WORKFLOW,CONNECT}_APP_POLICY_URL/_POLICY_SERVICE_TOKEN;
ohne URL Warnung und keine Pruefung (Uebergang), URL ohne Token Startfehler;
connect-app setzt die bestehende statusapi-Pruefung damit ebenfalls scharf
- Tests je API: 403 bei Ablehnung, 200 bei Freigabe, 503 bei Ausfall, Policy
wird ohne Identitaet/Service-Token nicht befragt
- MODULE_REGISTRY: ai_search_chat, workflow_tasks, connect_setup (+ Icons,
Portal-Rechteliste)
- Doku: BETRIEB-PORTAL.md, ENG-11 Umgebungsvariablen
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
176 lines
5.5 KiB
Go
176 lines
5.5 KiB
Go
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"net"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/jackc/pgx/v5/pgxpool"
|
|
|
|
"gitea.perlbach24.de/scripte/nexarch/common/testdb"
|
|
"gitea.perlbach24.de/scripte/nexarch/workflow/internal/migrate"
|
|
"gitea.perlbach24.de/scripte/nexarch/workflow/internal/task"
|
|
)
|
|
|
|
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 testPool(t *testing.T) *pgxpool.Pool {
|
|
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 pool
|
|
}
|
|
|
|
// Akzeptanzkriterium 1/4: cmd/workflow-app bindet alle Workflow-API-Pakete
|
|
// an einen einzigen ServeMux, jedes Paket beantwortet mindestens einen
|
|
// Endpunkt.
|
|
func TestBuildMux_AllPackagesAnswer(t *testing.T) {
|
|
pool := testPool(t)
|
|
taskStore := task.NewStore(pool)
|
|
mux, err := buildMux(pool, taskStore, "test-service-token", "mandant-a", "http://127.0.0.1:0", "", nil)
|
|
if err != nil {
|
|
t.Fatalf("buildMux: %v", err)
|
|
}
|
|
ts := httptest.NewServer(mux)
|
|
defer ts.Close()
|
|
|
|
do := func(method, path, userID, serviceToken string, body []byte) *http.Response {
|
|
req, err := http.NewRequest(method, ts.URL+path, bytes.NewReader(body))
|
|
if err != nil {
|
|
t.Fatalf("request bauen: %v", err)
|
|
}
|
|
if userID != "" {
|
|
req.Header.Set("X-User-Id", userID)
|
|
req.Header.Set("X-Tenant-Slug", "mandant-a")
|
|
}
|
|
if serviceToken != "" {
|
|
req.Header.Set("X-Service-Token", serviceToken)
|
|
}
|
|
if len(body) > 0 {
|
|
req.Header.Set("Content-Type", "application/json")
|
|
}
|
|
resp, err := ts.Client().Do(req)
|
|
if err != nil {
|
|
t.Fatalf("%s %s: %v", method, path, err)
|
|
}
|
|
return resp
|
|
}
|
|
|
|
// healthz
|
|
if resp := do(http.MethodGet, "/healthz", "", "", nil); resp.StatusCode != http.StatusOK {
|
|
t.Errorf("/healthz: status = %d, want 200", resp.StatusCode)
|
|
}
|
|
|
|
// caseapi: POST /cases
|
|
caseBody, _ := json.Marshal(map[string]any{"case_number": "A-1", "responsible": "frank"})
|
|
respCase := do(http.MethodPost, "/cases", "frank", "", caseBody)
|
|
if respCase.StatusCode != http.StatusCreated {
|
|
t.Errorf("caseapi POST /cases: status = %d, want 201", respCase.StatusCode)
|
|
}
|
|
|
|
// approvalapi: GET /approvals
|
|
if resp := do(http.MethodGet, "/approvals", "frank", "", nil); resp.StatusCode != http.StatusOK {
|
|
t.Errorf("approvalapi GET /approvals: status = %d, want 200", resp.StatusCode)
|
|
}
|
|
|
|
// caseoverviewapi: GET /cases
|
|
if resp := do(http.MethodGet, "/cases", "frank", "", nil); resp.StatusCode != http.StatusOK {
|
|
t.Errorf("caseoverviewapi GET /cases: status = %d, want 200", resp.StatusCode)
|
|
}
|
|
|
|
// routingruleapi: GET /routing-rules
|
|
if resp := do(http.MethodGet, "/routing-rules", "frank", "", nil); resp.StatusCode != http.StatusOK {
|
|
t.Errorf("routingruleapi GET /routing-rules: status = %d, want 200", resp.StatusCode)
|
|
}
|
|
|
|
// taskapi (Issue #369): GET /tasks
|
|
if resp := do(http.MethodGet, "/tasks", "frank", "", nil); resp.StatusCode != http.StatusOK {
|
|
t.Errorf("taskapi GET /tasks: status = %d, want 200", resp.StatusCode)
|
|
}
|
|
|
|
// moduleadapter: GET /registrations
|
|
if resp := do(http.MethodGet, "/registrations", "", "test-service-token", nil); resp.StatusCode != http.StatusOK {
|
|
t.Errorf("moduleadapter GET /registrations: status = %d, want 200", resp.StatusCode)
|
|
}
|
|
|
|
// backchannelapi: die tenant-Registry ist in diesem Test nicht real
|
|
// erreichbar -- Nachweis, dass die Route real gebunden ist (eigene
|
|
// Fehlermeldung statt generischer 404-Mux-Antwort), reicht fuer AC4.
|
|
respBC := do(http.MethodGet, "/objects/dms/document/1/context", "", "", nil)
|
|
if respBC.StatusCode != http.StatusUnauthorized {
|
|
t.Errorf("backchannelapi GET /objects/...: status = %d, want 401 (eigene Auth-Antwort, kein Mux-404)", respBC.StatusCode)
|
|
}
|
|
}
|
|
|
|
// Akzeptanzkriterium 3 / Pflichtpruefung 2: fehlende Pflicht-Umgebungsvariable
|
|
// fuehrt zu einer klaren Fehlermeldung und keinem offenen Port, kein
|
|
// stiller Absturz.
|
|
func TestMain_MissingRequiredEnv_FailsClosedWithClearMessage(t *testing.T) {
|
|
root := findModuleRoot(t)
|
|
addr := "127.0.0.1:19601"
|
|
|
|
cmd := exec.Command("go", "run", "./cmd/workflow-app")
|
|
cmd.Dir = root
|
|
cmd.Env = append(os.Environ(),
|
|
"NEXARCH_WORKFLOW_APP_LISTEN_ADDR="+addr,
|
|
// NEXARCH_WORKFLOW_APP_TENANT_DSN bewusst nicht gesetzt.
|
|
"NEXARCH_WORKFLOW_APP_SERVICE_TOKEN=irrelevant",
|
|
"NEXARCH_WORKFLOW_APP_TENANT_SLUG=irrelevant",
|
|
"NEXARCH_TENANT_REGISTRY_URL=http://127.0.0.1:0",
|
|
)
|
|
var out bytes.Buffer
|
|
cmd.Stdout = &out
|
|
cmd.Stderr = &out
|
|
|
|
err := cmd.Run()
|
|
if err == nil {
|
|
t.Fatal("workflow-app haette mit fehlendem NEXARCH_WORKFLOW_APP_TENANT_DSN nicht erfolgreich beenden duerfen")
|
|
}
|
|
if !strings.Contains(out.String(), "NEXARCH_WORKFLOW_APP_TENANT_DSN") {
|
|
t.Errorf("erwarte klare Fehlermeldung zur fehlenden Variable, habe: %s", out.String())
|
|
}
|
|
|
|
conn, dialErr := net.DialTimeout("tcp", addr, 500*time.Millisecond)
|
|
if dialErr == nil {
|
|
conn.Close()
|
|
t.Fatalf("port %s war trotz fehlender Pflichtvariable offen", addr)
|
|
}
|
|
}
|