Admins can manage built-in and custom actions with placeholders, ENV vars, and actions.create/update/delete permissions.
226 lines
7.3 KiB
Go
226 lines
7.3 KiB
Go
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)
|
|
}
|
|
}
|