package api import ( "bytes" "encoding/json" "net/http" "net/http/httptest" "testing" "codeberg.org/SquidSE/ClusterCanvas/service/internal/settings" ) type groupsResponseTest struct { Groups []settings.Group `json:"groups"` } type apiErrorResponseTest struct { Error string `json:"error"` } func TestGroupsGetEmptyWhenNoGroups(t *testing.T) { configDir := t.TempDir() cookie := seedCompletedSetup(t, configDir) router := NewRouter(configDir) request := withSession(httptest.NewRequest(http.MethodGet, "/api/v1/groups", nil), cookie) recorder := httptest.NewRecorder() router.ServeHTTP(recorder, request) if recorder.Code != http.StatusOK { t.Fatalf("expected status %d, got %d", http.StatusOK, recorder.Code) } var payload groupsResponseTest if err := json.NewDecoder(recorder.Body).Decode(&payload); err != nil { t.Fatalf("decode response: %v", err) } if payload.Groups == nil || len(payload.Groups) != 0 { t.Fatalf("expected empty groups list, got %#v", payload.Groups) } } func TestGroupsUpsertRejectsInvalidPermissions(t *testing.T) { configDir := t.TempDir() cookie := seedCompletedSetup(t, configDir) router := NewRouter(configDir) body := []byte(`{ "group": { "name": "Admins", "scope_kind": "node", "scope_name": "node-1", "permissions": ["nodes.nope"] } }`) request := withSession(httptest.NewRequest(http.MethodPost, "/api/v1/groups", bytes.NewReader(body)), cookie) recorder := httptest.NewRecorder() router.ServeHTTP(recorder, request) if recorder.Code != http.StatusBadRequest { t.Fatalf("expected status %d, got %d", http.StatusBadRequest, recorder.Code) } var payload apiErrorResponseTest if err := json.NewDecoder(recorder.Body).Decode(&payload); err != nil { t.Fatalf("decode response: %v", err) } if payload.Error == "" { t.Fatal("expected error message") } } func TestGroupsUpsertUpsertsByName(t *testing.T) { configDir := t.TempDir() cookie := seedCompletedSetup(t, configDir) router := NewRouter(configDir) createBody := []byte(`{ "group": { "name": "Admins", "scope_kind": "node", "scope_name": "node-1", "permissions": ["nodes.read"] } }`) createReq := withSession(httptest.NewRequest(http.MethodPost, "/api/v1/groups", bytes.NewReader(createBody)), cookie) createRec := httptest.NewRecorder() router.ServeHTTP(createRec, createReq) if createRec.Code != http.StatusOK { t.Fatalf("expected status %d, got %d", http.StatusOK, createRec.Code) } updateBody := []byte(`{ "group": { "name": "Admins", "scope_kind": "node", "scope_name": "node-2", "permissions": ["nodes.exec","nodes.update"] } }`) updateReq := withSession(httptest.NewRequest(http.MethodPost, "/api/v1/groups", bytes.NewReader(updateBody)), cookie) updateRec := httptest.NewRecorder() router.ServeHTTP(updateRec, updateReq) if updateRec.Code != http.StatusOK { t.Fatalf("expected status %d, got %d", http.StatusOK, updateRec.Code) } getReq := withSession(httptest.NewRequest(http.MethodGet, "/api/v1/groups", nil), cookie) getRec := httptest.NewRecorder() router.ServeHTTP(getRec, getReq) if getRec.Code != http.StatusOK { t.Fatalf("expected status %d, got %d", http.StatusOK, getRec.Code) } var getPayload groupsResponseTest if err := json.NewDecoder(getRec.Body).Decode(&getPayload); err != nil { t.Fatalf("decode response: %v", err) } if len(getPayload.Groups) != 1 { t.Fatalf("expected 1 group, got %d", len(getPayload.Groups)) } if getPayload.Groups[0].Name != "Admins" { t.Fatalf("expected group name %q, got %q", "Admins", getPayload.Groups[0].Name) } if getPayload.Groups[0].ScopeName != "node-2" { t.Fatalf("expected scope_name %q, got %q", "node-2", getPayload.Groups[0].ScopeName) } } func TestGroupsDeleteRemovesByName(t *testing.T) { configDir := t.TempDir() cookie := seedCompletedSetup(t, configDir) router := NewRouter(configDir) createBody := []byte(`{ "group": { "name": "Admins", "scope_kind": "node", "scope_name": "node-1", "permissions": ["nodes.read"] } }`) createReq := withSession(httptest.NewRequest(http.MethodPost, "/api/v1/groups", bytes.NewReader(createBody)), cookie) createRec := httptest.NewRecorder() router.ServeHTTP(createRec, createReq) if createRec.Code != http.StatusOK { t.Fatalf("expected status %d, got %d", http.StatusOK, createRec.Code) } deleteReq := withSession(httptest.NewRequest(http.MethodDelete, "/api/v1/groups?name=Admins", nil), cookie) deleteRec := httptest.NewRecorder() router.ServeHTTP(deleteRec, deleteReq) if deleteRec.Code != http.StatusOK { t.Fatalf("expected status %d, got %d", http.StatusOK, deleteRec.Code) } getReq := withSession(httptest.NewRequest(http.MethodGet, "/api/v1/groups", nil), cookie) getRec := httptest.NewRecorder() router.ServeHTTP(getRec, getReq) if getRec.Code != http.StatusOK { t.Fatalf("expected status %d, got %d", http.StatusOK, getRec.Code) } var getPayload groupsResponseTest if err := json.NewDecoder(getRec.Body).Decode(&getPayload); err != nil { t.Fatalf("decode response: %v", err) } if len(getPayload.Groups) != 0 { t.Fatalf("expected empty groups, got %#v", getPayload.Groups) } } func TestGroupsDeleteMissingNameFails(t *testing.T) { configDir := t.TempDir() cookie := seedCompletedSetup(t, configDir) router := NewRouter(configDir) deleteReq := withSession(httptest.NewRequest(http.MethodDelete, "/api/v1/groups", nil), cookie) deleteRec := httptest.NewRecorder() router.ServeHTTP(deleteRec, deleteReq) if deleteRec.Code != http.StatusBadRequest { t.Fatalf("expected status %d, got %d", http.StatusBadRequest, deleteRec.Code) } var payload apiErrorResponseTest if err := json.NewDecoder(deleteRec.Body).Decode(&payload); err != nil { t.Fatalf("decode response: %v", err) } if payload.Error == "" { t.Fatal("expected error message") } } func TestGroupsDeleteRejectsAdministrators(t *testing.T) { configDir := t.TempDir() cookie := seedCompletedSetup(t, configDir) router := NewRouter(configDir) createBody := []byte(`{ "group": { "name": "Administrators", "scope_kind": "group", "scope_name": "Admin Group", "permissions": ["nodes.read","users.manage"] } }`) createReq := withSession(httptest.NewRequest(http.MethodPost, "/api/v1/groups", bytes.NewReader(createBody)), cookie) createRec := httptest.NewRecorder() router.ServeHTTP(createRec, createReq) if createRec.Code != http.StatusOK { t.Fatalf("expected status %d, got %d body=%s", http.StatusOK, createRec.Code, createRec.Body.String()) } deleteReq := withSession( httptest.NewRequest(http.MethodDelete, "/api/v1/groups?name=Administrators", nil), cookie, ) deleteRec := httptest.NewRecorder() router.ServeHTTP(deleteRec, deleteReq) if deleteRec.Code != http.StatusConflict { t.Fatalf("expected status %d, got %d body=%s", http.StatusConflict, deleteRec.Code, deleteRec.Body.String()) } var payload apiErrorResponseTest if err := json.NewDecoder(deleteRec.Body).Decode(&payload); err != nil { t.Fatalf("decode response: %v", err) } if payload.Error != "Administrators group cannot be deleted" { t.Fatalf("unexpected error: %q", payload.Error) } getReq := withSession(httptest.NewRequest(http.MethodGet, "/api/v1/groups", nil), cookie) getRec := httptest.NewRecorder() router.ServeHTTP(getRec, getReq) var getPayload groupsResponseTest if err := json.NewDecoder(getRec.Body).Decode(&getPayload); err != nil { t.Fatalf("decode response: %v", err) } if len(getPayload.Groups) != 1 || getPayload.Groups[0].Name != settings.AdministratorsGroupName { t.Fatalf("Administrators group should still exist, got %#v", getPayload.Groups) } }