feat: Werkzeugkatalog-Pflege für die Plattform (Ebene 5)

Betreiber können den zentralen Werkzeugkatalog jetzt über die UI
pflegen (GET /betreiber/werkzeuge, GET/POST .../neu, GET/POST
.../{id}, POST .../{id}/loeschen) statt nur per SQL. Bearbeitet
ausschließlich zentrale (account_id IS NULL) Einträge — ein
mandantenspezifischer Katalogeintrag bleibt über diese Seiten
unerreichbar (404), das ist weiterhin Sache des jeweiligen Mandanten.
Ohne befüllten Katalog konnte bisher keine Bewertung tatsächlich zu
"genehmigt" mit einem echten Werkzeug führen.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
noroot
2026-08-29 12:42:48 +02:00
parent aad517ce23
commit cff8c9eadf
9 changed files with 542 additions and 5 deletions

View File

@@ -0,0 +1,118 @@
package web_test
import (
"context"
"net/http"
"net/url"
"strings"
"testing"
"github.com/netcell-it/deklarix/internal/store"
)
func werkzeugForm() url.Values {
return url.Values{
"name": {"ChatGPT Enterprise"}, "anbieter": {"OpenAI"}, "verarbeitungsort": {"EU"},
"avv_verfuegbar": {"on"}, "avv_url": {"https://example.com/avv"},
"training_opt_out": {"on"}, "training_standard": {"on"}, "aufbewahrung_tage": {"30"},
"zertifizierungen": {"ISO 27001, SOC 2"}, "geeignete_zwecke": {"Textgenerierung"},
"einschraenkungen": {""}, "letzte_pruefung": {"2026-08-01"}, "quelle": {"https://example.com/beleg"},
}
}
func TestBetreiberCanCreateZentralesWerkzeug(t *testing.T) {
fs := newFakeStore()
s := newServer(t, fs)
betreiberCookie := seedAccountWithRole(t, fs, "Deklarix Betreiber", "betreiber@example.com", "betreiber")
resp := postForm(t, s, betreiberCookie, "/betreiber/werkzeuge", werkzeugForm())
if resp.Code != http.StatusSeeOther {
t.Fatalf("status = %d, want 303, body: %s", resp.Code, resp.Body.String())
}
var w store.Werkzeug
for _, wz := range fs.werkzeuge {
w = wz
}
if w.Name != "ChatGPT Enterprise" || w.AccountID != nil {
t.Fatalf("Werkzeug = %+v, unerwartete Werte", w)
}
if len(w.Zertifizierungen) != 2 {
t.Errorf("Zertifizierungen = %v, want 2 Einträge", w.Zertifizierungen)
}
list := getWithCookie(t, s, betreiberCookie, "/betreiber/werkzeuge")
if !strings.Contains(list.Body.String(), "ChatGPT Enterprise") {
t.Errorf("expected the new werkzeug in the Katalogliste, got: %s", list.Body.String())
}
}
func TestBetreiberWerkzeugCreateRequiresLetztePruefungUndQuelle(t *testing.T) {
fs := newFakeStore()
s := newServer(t, fs)
betreiberCookie := seedAccountWithRole(t, fs, "Deklarix Betreiber", "betreiber@example.com", "betreiber")
form := werkzeugForm()
form.Set("letzte_pruefung", "")
resp := postForm(t, s, betreiberCookie, "/betreiber/werkzeuge", form)
if resp.Code != http.StatusOK {
t.Fatalf("status = %d, want 200 (Formular mit Fehler) ohne letzte_pruefung", resp.Code)
}
if len(fs.werkzeuge) != 0 {
t.Errorf("expected no werkzeug to be created, got %d", len(fs.werkzeuge))
}
}
func TestBetreiberCanUpdateAndDeleteZentralesWerkzeug(t *testing.T) {
fs := newFakeStore()
s := newServer(t, fs)
betreiberCookie := seedAccountWithRole(t, fs, "Deklarix Betreiber", "betreiber@example.com", "betreiber")
postForm(t, s, betreiberCookie, "/betreiber/werkzeuge", werkzeugForm())
var id string
for wid := range fs.werkzeuge {
id = wid
}
editResp := getWithCookie(t, s, betreiberCookie, "/betreiber/werkzeuge/"+id)
if editResp.Code != http.StatusOK || !strings.Contains(editResp.Body.String(), "ChatGPT Enterprise") {
t.Fatalf("edit form status = %d, body: %s", editResp.Code, editResp.Body.String())
}
updateForm := werkzeugForm()
updateForm.Set("name", "ChatGPT Enterprise (aktualisiert)")
updateResp := postForm(t, s, betreiberCookie, "/betreiber/werkzeuge/"+id, updateForm)
if updateResp.Code != http.StatusSeeOther {
t.Fatalf("update status = %d, body: %s", updateResp.Code, updateResp.Body.String())
}
if fs.werkzeuge[id].Name != "ChatGPT Enterprise (aktualisiert)" {
t.Errorf("Name = %q nach Update, unerwartet", fs.werkzeuge[id].Name)
}
deleteResp := postForm(t, s, betreiberCookie, "/betreiber/werkzeuge/"+id+"/loeschen", url.Values{})
if deleteResp.Code != http.StatusSeeOther {
t.Fatalf("delete status = %d, body: %s", deleteResp.Code, deleteResp.Body.String())
}
if _, ok := fs.werkzeuge[id]; ok {
t.Error("expected the werkzeug to be deleted")
}
}
// TestBetreiberCannotEditMandantenEigenesWerkzeug: ein mandantenspezifischer
// Katalogeintrag (account_id gesetzt) ist über die Betreiber-Seiten
// nicht erreichbar — das bleibt Sache des jeweiligen Mandanten.
func TestBetreiberCannotEditMandantenEigenesWerkzeug(t *testing.T) {
fs := newFakeStore()
s := newServer(t, fs)
betreiberCookie := seedAccountWithRole(t, fs, "Deklarix Betreiber", "betreiber@example.com", "betreiber")
acc, err := fs.CreateAccount(context.Background(), "Mandant mit eigenem Werkzeug")
if err != nil {
t.Fatalf("CreateAccount: %v", err)
}
eigenesID := "eigenes-werkzeug"
fs.werkzeuge[eigenesID] = store.Werkzeug{ID: eigenesID, AccountID: &acc.ID, Name: "Mandanten-eigenes Tool"}
resp := getWithCookie(t, s, betreiberCookie, "/betreiber/werkzeuge/"+eigenesID)
if resp.Code != http.StatusNotFound {
t.Fatalf("status = %d, want 404 for a mandanten-eigenes werkzeug", resp.Code)
}
}