Add Configuration Actions tab for shell and script action definitions.
Admins can manage built-in and custom actions with placeholders, ENV vars, and actions.create/update/delete permissions.
This commit is contained in:
@@ -0,0 +1,225 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"codeberg.org/SquidSE/ClusterCanvas/service/internal/settings"
|
||||
)
|
||||
|
||||
type actionsResponseTest struct {
|
||||
Actions []settings.Action `json:"actions"`
|
||||
}
|
||||
|
||||
func TestActionsGetSeedsBuiltins(t *testing.T) {
|
||||
configDir := t.TempDir()
|
||||
cookie := seedCompletedSetup(t, configDir)
|
||||
router := NewRouter(configDir)
|
||||
|
||||
request := withSession(httptest.NewRequest(http.MethodGet, "/api/v1/actions", nil), cookie)
|
||||
recorder := httptest.NewRecorder()
|
||||
router.ServeHTTP(recorder, request)
|
||||
|
||||
if recorder.Code != http.StatusOK {
|
||||
t.Fatalf("expected status %d, got %d body=%s", http.StatusOK, recorder.Code, recorder.Body.String())
|
||||
}
|
||||
|
||||
var payload actionsResponseTest
|
||||
if err := json.NewDecoder(recorder.Body).Decode(&payload); err != nil {
|
||||
t.Fatalf("decode response: %v", err)
|
||||
}
|
||||
if len(payload.Actions) != 3 {
|
||||
t.Fatalf("expected 3 builtins, got %d", len(payload.Actions))
|
||||
}
|
||||
for _, action := range payload.Actions {
|
||||
if !action.Builtin {
|
||||
t.Fatalf("expected builtin, got %#v", action)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestActionsCreatePatchDelete(t *testing.T) {
|
||||
configDir := t.TempDir()
|
||||
cookie := seedCompletedSetup(t, configDir)
|
||||
router := NewRouter(configDir)
|
||||
|
||||
createBody := []byte(`{
|
||||
"name": "Echo host",
|
||||
"description": "Print the target IP",
|
||||
"kind": "shell",
|
||||
"body": "echo {{node.ip}}",
|
||||
"env": [{"name": "APP_HOME", "value": "/opt/app"}]
|
||||
}`)
|
||||
createReq := withSession(httptest.NewRequest(http.MethodPost, "/api/v1/actions", bytes.NewReader(createBody)), cookie)
|
||||
createRec := httptest.NewRecorder()
|
||||
router.ServeHTTP(createRec, createReq)
|
||||
if createRec.Code != http.StatusOK {
|
||||
t.Fatalf("create status %d body=%s", createRec.Code, createRec.Body.String())
|
||||
}
|
||||
|
||||
var created actionsResponseTest
|
||||
if err := json.NewDecoder(createRec.Body).Decode(&created); err != nil {
|
||||
t.Fatalf("decode create: %v", err)
|
||||
}
|
||||
if len(created.Actions) != 4 {
|
||||
t.Fatalf("expected 4 actions after create, got %d", len(created.Actions))
|
||||
}
|
||||
custom := created.Actions[3]
|
||||
if custom.Builtin || custom.Name != "Echo host" || custom.Kind != settings.ActionKindShell {
|
||||
t.Fatalf("custom = %#v", custom)
|
||||
}
|
||||
|
||||
patchBody := []byte(`{"name":"Echo target","body":"echo {{node.host}}"}`)
|
||||
patchReq := withSession(
|
||||
httptest.NewRequest(http.MethodPatch, "/api/v1/actions?id="+custom.ID, bytes.NewReader(patchBody)),
|
||||
cookie,
|
||||
)
|
||||
patchRec := httptest.NewRecorder()
|
||||
router.ServeHTTP(patchRec, patchReq)
|
||||
if patchRec.Code != http.StatusOK {
|
||||
t.Fatalf("patch status %d body=%s", patchRec.Code, patchRec.Body.String())
|
||||
}
|
||||
|
||||
var patched actionsResponseTest
|
||||
if err := json.NewDecoder(patchRec.Body).Decode(&patched); err != nil {
|
||||
t.Fatalf("decode patch: %v", err)
|
||||
}
|
||||
found := false
|
||||
for _, action := range patched.Actions {
|
||||
if action.ID == custom.ID {
|
||||
found = true
|
||||
if action.Name != "Echo target" || action.Body != "echo {{node.host}}" {
|
||||
t.Fatalf("patched = %#v", action)
|
||||
}
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Fatal("patched action missing")
|
||||
}
|
||||
|
||||
deleteReq := withSession(
|
||||
httptest.NewRequest(http.MethodDelete, "/api/v1/actions?id="+custom.ID, nil),
|
||||
cookie,
|
||||
)
|
||||
deleteRec := httptest.NewRecorder()
|
||||
router.ServeHTTP(deleteRec, deleteReq)
|
||||
if deleteRec.Code != http.StatusOK {
|
||||
t.Fatalf("delete status %d body=%s", deleteRec.Code, deleteRec.Body.String())
|
||||
}
|
||||
|
||||
var deleted actionsResponseTest
|
||||
if err := json.NewDecoder(deleteRec.Body).Decode(&deleted); err != nil {
|
||||
t.Fatalf("decode delete: %v", err)
|
||||
}
|
||||
if len(deleted.Actions) != 3 {
|
||||
t.Fatalf("expected 3 after delete, got %d", len(deleted.Actions))
|
||||
}
|
||||
}
|
||||
|
||||
func TestActionsRejectBuiltinMutationAndInvalidInput(t *testing.T) {
|
||||
configDir := t.TempDir()
|
||||
cookie := seedCompletedSetup(t, configDir)
|
||||
router := NewRouter(configDir)
|
||||
|
||||
getReq := withSession(httptest.NewRequest(http.MethodGet, "/api/v1/actions", nil), cookie)
|
||||
getRec := httptest.NewRecorder()
|
||||
router.ServeHTTP(getRec, getReq)
|
||||
var listed actionsResponseTest
|
||||
if err := json.NewDecoder(getRec.Body).Decode(&listed); err != nil {
|
||||
t.Fatalf("decode list: %v", err)
|
||||
}
|
||||
builtinID := listed.Actions[0].ID
|
||||
|
||||
patchBody := []byte(`{"name":"Nope"}`)
|
||||
patchReq := withSession(
|
||||
httptest.NewRequest(http.MethodPatch, "/api/v1/actions?id="+builtinID, bytes.NewReader(patchBody)),
|
||||
cookie,
|
||||
)
|
||||
patchRec := httptest.NewRecorder()
|
||||
router.ServeHTTP(patchRec, patchReq)
|
||||
if patchRec.Code != http.StatusBadRequest {
|
||||
t.Fatalf("expected builtin patch 400, got %d", patchRec.Code)
|
||||
}
|
||||
|
||||
deleteReq := withSession(
|
||||
httptest.NewRequest(http.MethodDelete, "/api/v1/actions?id="+builtinID, nil),
|
||||
cookie,
|
||||
)
|
||||
deleteRec := httptest.NewRecorder()
|
||||
router.ServeHTTP(deleteRec, deleteReq)
|
||||
if deleteRec.Code != http.StatusBadRequest {
|
||||
t.Fatalf("expected builtin delete 400, got %d", deleteRec.Code)
|
||||
}
|
||||
|
||||
badCreate := []byte(`{"name":"x","kind":"shell","body":"echo","env":[{"name":"bad-name","value":"1"}]}`)
|
||||
badReq := withSession(httptest.NewRequest(http.MethodPost, "/api/v1/actions", bytes.NewReader(badCreate)), cookie)
|
||||
badRec := httptest.NewRecorder()
|
||||
router.ServeHTTP(badRec, badReq)
|
||||
if badRec.Code != http.StatusBadRequest {
|
||||
t.Fatalf("expected invalid env 400, got %d", badRec.Code)
|
||||
}
|
||||
|
||||
invalidKind := []byte(`{"name":"x","kind":"nope","body":"echo"}`)
|
||||
kindReq := withSession(httptest.NewRequest(http.MethodPost, "/api/v1/actions", bytes.NewReader(invalidKind)), cookie)
|
||||
kindRec := httptest.NewRecorder()
|
||||
router.ServeHTTP(kindRec, kindReq)
|
||||
if kindRec.Code != http.StatusBadRequest {
|
||||
t.Fatalf("expected invalid kind 400, got %d", kindRec.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestActionsMutationsRequirePermissions(t *testing.T) {
|
||||
configDir := t.TempDir()
|
||||
cookie := seedCompletedSetup(t, configDir)
|
||||
router := NewRouter(configDir)
|
||||
|
||||
// Strip action permissions from Administrators.
|
||||
settingsPayload, err := settings.LoadSettings(configDir)
|
||||
if err != nil {
|
||||
t.Fatalf("LoadSettings: %v", err)
|
||||
}
|
||||
for index := range settingsPayload.Groups {
|
||||
if settingsPayload.Groups[index].Name != settings.AdministratorsGroupName {
|
||||
continue
|
||||
}
|
||||
filtered := make([]string, 0, len(settingsPayload.Groups[index].Permissions))
|
||||
for _, permission := range settingsPayload.Groups[index].Permissions {
|
||||
if permission == "actions.create" || permission == "actions.update" || permission == "actions.delete" {
|
||||
continue
|
||||
}
|
||||
filtered = append(filtered, permission)
|
||||
}
|
||||
settingsPayload.Groups[index].Permissions = filtered
|
||||
}
|
||||
if err := settings.SaveSettings(configDir, settingsPayload); err != nil {
|
||||
t.Fatalf("SaveSettings: %v", err)
|
||||
}
|
||||
|
||||
createBody := []byte(`{"name":"x","kind":"shell","body":"echo"}`)
|
||||
createReq := withSession(httptest.NewRequest(http.MethodPost, "/api/v1/actions", bytes.NewReader(createBody)), cookie)
|
||||
createRec := httptest.NewRecorder()
|
||||
router.ServeHTTP(createRec, createReq)
|
||||
if createRec.Code != http.StatusForbidden {
|
||||
t.Fatalf("expected create 403, got %d body=%s", createRec.Code, createRec.Body.String())
|
||||
}
|
||||
|
||||
patchReq := withSession(
|
||||
httptest.NewRequest(http.MethodPatch, "/api/v1/actions?id=custom", bytes.NewReader([]byte(`{"name":"y"}`))),
|
||||
cookie,
|
||||
)
|
||||
patchRec := httptest.NewRecorder()
|
||||
router.ServeHTTP(patchRec, patchReq)
|
||||
if patchRec.Code != http.StatusForbidden {
|
||||
t.Fatalf("expected patch 403, got %d", patchRec.Code)
|
||||
}
|
||||
|
||||
deleteReq := withSession(httptest.NewRequest(http.MethodDelete, "/api/v1/actions?id=custom", nil), cookie)
|
||||
deleteRec := httptest.NewRecorder()
|
||||
router.ServeHTTP(deleteRec, deleteReq)
|
||||
if deleteRec.Code != http.StatusForbidden {
|
||||
t.Fatalf("expected delete 403, got %d", deleteRec.Code)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user