Files
nexarch/workflow/internal/taskapi/taskapi_test.go
patrickandClaude Sonnet 5 56ab1a2d6d
CI / Web-Barrierefreiheitspruefung (axe, Tastatur, Reflow, Textabstaende) (push) Canceled after 0s
CI / Web-Barrierefreiheitspruefung (axe, Tastatur, Reflow, Textabstaende) (pull_request) Canceled after 0s
CI / DMS-Modul Supply-Chain-Scan (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 / DMS-Modul Supply-Chain-Scan (govulncheck) (pull_request) Canceled after 0s
CI / Web (dms-browser) (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 / 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
Supply-Chain-Scan (npm) / npm-audit (push) 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) (pull_request) Canceled after 0s
CI / DMS-Modul (Lint/Build/Test/Coverage/Artefakt) (push) Canceled after 0s
TSK-04: aufgaben oberflaeche
Neue additive status-Spalte (open/in_progress/done) auf tasks,
status.go (SetStatus/ByStatus/GetStatus/ListAll) haelt completed_at
synchron fuer TSK-03s escalation.Store. Neuer HTTP-Layer taskapi
(GET /tasks, PATCH /tasks/{id}/status), Tenant-Kontext-Durchsetzung
wie backchannelapi (VOR-06). Neue Next.js-Oberflaeche
web/workflow-tasks (Liste + Kanban, Drag&Drop + Tastaturbedienung).
tasks-a11y-fixture fuer den echten Browser-Test, kein Produktions-
binary vorhanden das geaendert werden musste.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-26 23:26:56 +02:00

186 lines
5.1 KiB
Go

package taskapi
import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"strings"
"testing"
"gitea.perlbach24.de/scripte/nexarch/workflow/internal/migrate"
"gitea.perlbach24.de/scripte/nexarch/workflow/internal/task"
"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, *task.Store) {
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)
}
store := task.NewStore(pool)
return NewServer(store, "acme"), store
}
func TestHandleList_RequiresAuth(t *testing.T) {
srv, _ := newTestServer(t)
mux := http.NewServeMux()
srv.RegisterRoutes(mux)
req := httptest.NewRequest(http.MethodGet, "/tasks", nil)
w := httptest.NewRecorder()
mux.ServeHTTP(w, req)
if w.Code != http.StatusUnauthorized {
t.Fatalf("erwartet 401 ohne X-User-Id/X-Tenant-Slug, bekam %d", w.Code)
}
}
func TestHandleList_RejectsForeignTenant(t *testing.T) {
srv, _ := newTestServer(t)
mux := http.NewServeMux()
srv.RegisterRoutes(mux)
req := httptest.NewRequest(http.MethodGet, "/tasks", nil)
req.Header.Set("X-User-Id", "u1")
req.Header.Set("X-Tenant-Slug", "fremd")
w := httptest.NewRecorder()
mux.ServeHTTP(w, req)
if w.Code != http.StatusForbidden {
t.Fatalf("erwartet 403 bei fremdem tenant, bekam %d", w.Code)
}
}
func TestHandleList_ReturnsTasksForOwnTenant(t *testing.T) {
srv, store := newTestServer(t)
ctx := context.Background()
id, err := store.Create(ctx, task.Task{Title: "API-Liste-Test", AssigneeType: task.AssigneePerson, AssigneeID: "u1"})
if err != nil {
t.Fatalf("create: %v", err)
}
mux := http.NewServeMux()
srv.RegisterRoutes(mux)
req := httptest.NewRequest(http.MethodGet, "/tasks", nil)
req.Header.Set("X-User-Id", "u1")
req.Header.Set("X-Tenant-Slug", "acme")
w := httptest.NewRecorder()
mux.ServeHTTP(w, req)
if w.Code != http.StatusOK {
t.Fatalf("erwartet 200, bekam %d: %s", w.Code, w.Body.String())
}
var tasks []task.Task
if err := json.NewDecoder(w.Body).Decode(&tasks); err != nil {
t.Fatalf("decode: %v", err)
}
found := false
for _, tk := range tasks {
if tk.ID == id {
found = true
}
}
if !found {
t.Fatal("erstellte aufgabe fehlt in der antwort")
}
}
func TestHandleSetStatus_DragAndDropUpdatesViaTSK01(t *testing.T) {
srv, store := newTestServer(t)
ctx := context.Background()
id, err := store.Create(ctx, task.Task{Title: "Drag-Test", AssigneeType: task.AssigneePerson, AssigneeID: "u1"})
if err != nil {
t.Fatalf("create: %v", err)
}
mux := http.NewServeMux()
srv.RegisterRoutes(mux)
body := strings.NewReader(`{"status":"in_progress"}`)
req := httptest.NewRequest(http.MethodPatch, "/tasks/"+id+"/status", body)
req.Header.Set("X-User-Id", "u1")
req.Header.Set("X-Tenant-Slug", "acme")
w := httptest.NewRecorder()
mux.ServeHTTP(w, req)
if w.Code != http.StatusNoContent {
t.Fatalf("erwartet 204, bekam %d: %s", w.Code, w.Body.String())
}
// Nachweis: der Status wurde tatsaechlich ueber TSK-01 (task.Store)
// zurueckgeschrieben, nicht nur im Handler simuliert.
got, err := store.Get(ctx, id)
if err != nil {
t.Fatalf("get: %v", err)
}
if got.Status != task.StatusInProgress {
t.Fatalf("erwartet in_progress nach drag&drop, war %q", got.Status)
}
}
func TestHandleSetStatus_UnknownTask(t *testing.T) {
srv, _ := newTestServer(t)
mux := http.NewServeMux()
srv.RegisterRoutes(mux)
body := strings.NewReader(`{"status":"done"}`)
req := httptest.NewRequest(http.MethodPatch, "/tasks/00000000-0000-0000-0000-000000000000/status", body)
req.Header.Set("X-User-Id", "u1")
req.Header.Set("X-Tenant-Slug", "acme")
w := httptest.NewRecorder()
mux.ServeHTTP(w, req)
if w.Code != http.StatusNotFound {
t.Fatalf("erwartet 404, bekam %d", w.Code)
}
}
func TestHandleSetStatus_InvalidStatus(t *testing.T) {
srv, store := newTestServer(t)
ctx := context.Background()
id, err := store.Create(ctx, task.Task{Title: "Ungueltig-API-Test", AssigneeType: task.AssigneePerson, AssigneeID: "u1"})
if err != nil {
t.Fatalf("create: %v", err)
}
mux := http.NewServeMux()
srv.RegisterRoutes(mux)
body := strings.NewReader(`{"status":"erfunden"}`)
req := httptest.NewRequest(http.MethodPatch, "/tasks/"+id+"/status", body)
req.Header.Set("X-User-Id", "u1")
req.Header.Set("X-Tenant-Slug", "acme")
w := httptest.NewRecorder()
mux.ServeHTTP(w, req)
if w.Code != http.StatusBadRequest {
t.Fatalf("erwartet 400, bekam %d", w.Code)
}
}