Files
ClusterCanvas/service/internal/api/users_handlers_test.go
T
Squid 2605c3b346 Add users and network settings tabs with admin protections.
List and delete accounts with last-admin and Administrators-group guards, and expose editable network settings via the configuration UI.
2026-07-18 15:18:01 +02:00

195 lines
5.5 KiB
Go

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)
}
}