Files
ClusterCanvas/service/internal/api/actions_handlers_test.go
T

260 lines
8.5 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) != 4 {
t.Fatalf("expected 4 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"}],
"requires_sudo": true
}`)
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) != 5 {
t.Fatalf("expected 5 actions after create, got %d", len(created.Actions))
}
custom := created.Actions[4]
if custom.Builtin || custom.Name != "Echo host" || custom.Kind != settings.ActionKindShell {
t.Fatalf("custom = %#v", custom)
}
if !custom.RequiresSudo {
t.Fatalf("expected requires_sudo true, got %#v", custom)
}
patchBody := []byte(`{"name":"Echo target","body":"echo {{node.host}}","requires_sudo":false}`)
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 action.RequiresSudo {
t.Fatalf("expected requires_sudo false after patch, got %#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) != 4 {
t.Fatalf("expected 4 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)
}
widgetBody := []byte(`{"show_widget":true,"widget_type":"raw"}`)
widgetReq := withSession(
httptest.NewRequest(http.MethodPatch, "/api/v1/actions?id="+builtinID, bytes.NewReader(widgetBody)),
cookie,
)
widgetRec := httptest.NewRecorder()
router.ServeHTTP(widgetRec, widgetReq)
if widgetRec.Code != http.StatusOK {
t.Fatalf("expected builtin widget patch 200, got %d body=%s", widgetRec.Code, widgetRec.Body.String())
}
var widgetPatched actionsResponseTest
if err := json.NewDecoder(widgetRec.Body).Decode(&widgetPatched); err != nil {
t.Fatalf("decode widget patch: %v", err)
}
foundWidget := false
for _, action := range widgetPatched.Actions {
if action.ID == builtinID {
foundWidget = true
if !action.ShowWidget || action.WidgetType != settings.ActionWidgetTypeRaw {
t.Fatalf("widget patched = %#v", action)
}
}
}
if !foundWidget {
t.Fatal("builtin missing after widget patch")
}
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)
}
}