package api import ( "encoding/json" "net/http" "net/http/httptest" "strings" "testing" "time" "codeberg.org/SquidSE/ClusterCanvas/service/internal/auth" "codeberg.org/SquidSE/ClusterCanvas/service/internal/settings" ) type usersResponseTest struct { Users []userPublic `json:"users"` } func TestUsersGetListsSanitizedFields(t *testing.T) { configDir := t.TempDir() cookie := seedCompletedSetup(t, configDir) router := NewRouter(configDir) request := withSession(httptest.NewRequest(http.MethodGet, "/api/v1/users", 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()) } rawBody := recorder.Body.Bytes() var payload usersResponseTest if err := json.Unmarshal(rawBody, &payload); err != nil { t.Fatalf("decode response: %v", err) } if len(payload.Users) != 1 { t.Fatalf("expected 1 user, got %d", len(payload.Users)) } user := payload.Users[0] if user.Username != "Admin" { t.Fatalf("expected username Admin, got %q", user.Username) } if !user.IsAdmin { t.Fatal("expected is_admin true") } if user.CanDelete { t.Fatal("expected can_delete false for sole admin") } if user.ID == "" || user.Username == "" { t.Fatal("expected id and username") } raw := string(rawBody) if strings.Contains(raw, "password_hash") || strings.Contains(raw, "totp_secret") { t.Fatalf("response leaked secrets: %s", raw) } } func TestUsersDeleteRejectsLastAdministrator(t *testing.T) { configDir := t.TempDir() cookie := seedCompletedSetup(t, configDir) router := NewRouter(configDir) request := withSession( httptest.NewRequest(http.MethodDelete, "/api/v1/users?id=test-user-id", nil), cookie, ) recorder := httptest.NewRecorder() router.ServeHTTP(recorder, request) if recorder.Code != http.StatusConflict { t.Fatalf("expected status %d, got %d body=%s", http.StatusConflict, recorder.Code, recorder.Body.String()) } var payload apiErrorResponseTest if err := json.NewDecoder(recorder.Body).Decode(&payload); err != nil { t.Fatalf("decode response: %v", err) } if payload.Error != "cannot delete the last administrator" { t.Fatalf("unexpected error: %q", payload.Error) } } func TestUsersDeleteAllowsNonAdminAndSecondAdmin(t *testing.T) { configDir := t.TempDir() cookie := seedCompletedSetup(t, configDir) key, err := settings.KeyFromEnv() if err != nil { t.Fatalf("KeyFromEnv: %v", err) } store, err := settings.LoadPasswords(configDir, key) if err != nil { t.Fatalf("LoadPasswords: %v", err) } now := time.Now().UTC() passwordHash, err := auth.HashPassword("correct horse battery staple extra") if err != nil { t.Fatalf("HashPassword: %v", err) } store.Users = append(store.Users, settings.UserCredential{ ID: "operator-id", Username: "operator", PasswordHash: passwordHash, Enabled: true, GroupNames: []string{"Operators"}, CreatedAt: now, PasswordChangedAt: now, }, settings.UserCredential{ ID: "admin-two-id", Username: "AdminTwo", PasswordHash: passwordHash, Enabled: true, GroupNames: []string{settings.AdministratorsGroupName}, CreatedAt: now, PasswordChangedAt: now, }, ) if err := settings.SavePasswords(configDir, store, key); err != nil { t.Fatalf("SavePasswords: %v", err) } router := NewRouter(configDir) deleteOperator := withSession( httptest.NewRequest(http.MethodDelete, "/api/v1/users?id=operator-id", nil), cookie, ) operatorRec := httptest.NewRecorder() router.ServeHTTP(operatorRec, deleteOperator) if operatorRec.Code != http.StatusOK { t.Fatalf("delete operator: expected %d, got %d body=%s", http.StatusOK, operatorRec.Code, operatorRec.Body.String()) } deleteSecondAdmin := withSession( httptest.NewRequest(http.MethodDelete, "/api/v1/users?id=admin-two-id", nil), cookie, ) adminRec := httptest.NewRecorder() router.ServeHTTP(adminRec, deleteSecondAdmin) if adminRec.Code != http.StatusOK { t.Fatalf("delete second admin: expected %d, got %d body=%s", http.StatusOK, adminRec.Code, adminRec.Body.String()) } var payload usersResponseTest if err := json.NewDecoder(adminRec.Body).Decode(&payload); err != nil { t.Fatalf("decode response: %v", err) } if len(payload.Users) != 1 { t.Fatalf("expected 1 remaining user, got %d", len(payload.Users)) } if payload.Users[0].ID != "test-user-id" { t.Fatalf("expected sole remaining admin id, got %q", payload.Users[0].ID) } if payload.Users[0].CanDelete { t.Fatal("expected remaining sole admin can_delete false") } } func TestUsersDeleteMissingIDFails(t *testing.T) { configDir := t.TempDir() cookie := seedCompletedSetup(t, configDir) router := NewRouter(configDir) request := withSession(httptest.NewRequest(http.MethodDelete, "/api/v1/users", nil), cookie) recorder := httptest.NewRecorder() router.ServeHTTP(recorder, request) if recorder.Code != http.StatusBadRequest { t.Fatalf("expected status %d, got %d", http.StatusBadRequest, recorder.Code) } } func TestUsersDeleteUnknownUserNotFound(t *testing.T) { configDir := t.TempDir() cookie := seedCompletedSetup(t, configDir) router := NewRouter(configDir) request := withSession( httptest.NewRequest(http.MethodDelete, "/api/v1/users?id=missing", nil), cookie, ) recorder := httptest.NewRecorder() router.ServeHTTP(recorder, request) if recorder.Code != http.StatusNotFound { t.Fatalf("expected status %d, got %d", http.StatusNotFound, recorder.Code) } }