vdi-cloud/platform/internal/server/administrators_test.go

196 lines
8.4 KiB
Go

package server
import (
"context"
"encoding/json"
"strings"
"sync"
"testing"
"vdi-cloud/platform/internal/store"
)
const testAccountPassword = "Account-test-2026"
func createTestAccount(t *testing.T, f *fixture, token, username, role string) string {
t.Helper()
status, out := f.request("POST", "/administrators", token, map[string]any{"username": username, "displayName": "Account test", "role": role, "password": testAccountPassword}, "")
expect(t, 201, status, out)
for _, field := range []string{"password", "passwordHash", "password_hash"} {
if _, ok := out[field]; ok {
t.Fatal("credential leaked", field)
}
}
return out["id"].(string)
}
func accountLogin(t *testing.T, f *fixture, username, password string) string {
t.Helper()
status, out := f.request("POST", "/auth/login", "", map[string]any{"username": username, "password": password}, "")
expect(t, 200, status, out)
return out["accessToken"].(string)
}
func TestAdministratorLifecycleAndSessionRevocation(t *testing.T) {
f := newFixture(t)
admin := f.login("admin")
id := createTestAccount(t, f, admin, "maintainer", "operator")
token := accountLogin(t, f, "maintainer", testAccountPassword)
status, out := f.request("GET", "/administrators", token, nil, "")
expect(t, 403, status, out)
status, out = f.request("PUT", "/administrators/"+id, admin, map[string]any{"displayName": "Read-only", "role": "viewer", "enabled": true}, "")
expect(t, 200, status, out)
status, out = f.request("GET", "/auth/me", token, nil, "")
expect(t, 401, status, out)
token = accountLogin(t, f, "maintainer", testAccountPassword)
status, out = f.request("POST", "/desktops/desk_demo_kylin/actions", token, map[string]any{"action": "start"}, "viewer-no-power")
expect(t, 403, status, out)
status, out = f.request("POST", "/administrators/"+id+"/password", admin, map[string]any{"password": "Reset-test-2026"}, "")
expect(t, 204, status, out)
status, out = f.request("GET", "/auth/me", token, nil, "")
expect(t, 401, status, out)
status, out = f.request("POST", "/auth/login", "", map[string]any{"username": "maintainer", "password": testAccountPassword}, "")
expect(t, 401, status, out)
token = accountLogin(t, f, "maintainer", "Reset-test-2026")
status, out = f.request("PUT", "/administrators/"+id, admin, map[string]any{"displayName": "Disabled", "role": "viewer", "enabled": false}, "")
expect(t, 200, status, out)
status, out = f.request("GET", "/auth/me", token, nil, "")
expect(t, 401, status, out)
status, out = f.request("POST", "/auth/login", "", map[string]any{"username": "maintainer", "password": "Reset-test-2026"}, "")
expect(t, 401, status, out)
if err := store.Bootstrap(context.Background(), f.db, "demo", "", ""); err != nil {
t.Fatal(err)
}
var enabled bool
if err := f.db.Get(&enabled, "SELECT enabled FROM administrators WHERE id=$1", id); err != nil {
t.Fatal(err)
}
if enabled {
t.Fatal("restart re-enabled disabled account")
}
status, out = f.request("PUT", "/administrators/"+id, admin, map[string]any{"displayName": "Restored", "role": "operator", "enabled": true}, "")
expect(t, 200, status, out)
_ = accountLogin(t, f, "maintainer", "Reset-test-2026")
var audit []string
if err := f.db.Select(&audit, "SELECT action||':'||target FROM audit_logs WHERE target='maintainer' AND action LIKE '%administrator%'"); err != nil {
t.Fatal(err)
}
if len(audit) != 5 {
t.Fatal("account changes not audited", audit)
}
status, out = f.request("GET", "/administrators", admin, nil, "")
expect(t, 200, status, out)
encoded, _ := json.Marshal(out)
if strings.Contains(string(encoded), "$2") || strings.Contains(string(encoded), testAccountPassword) {
t.Fatal("credential material in account listing")
}
}
func TestOwnPasswordRequiresCurrentPasswordAndInvalidatesAllSessions(t *testing.T) {
f := newFixture(t)
token := f.login("viewer")
other := f.login("viewer")
status, out := f.request("POST", "/auth/password", token, map[string]any{"currentPassword": "wrong-password", "newPassword": "Updated-test-2026"}, "")
expect(t, 400, status, out)
status, out = f.request("GET", "/auth/me", token, nil, "")
expect(t, 200, status, out)
status, out = f.request("POST", "/auth/password", token, map[string]any{"currentPassword": "Demo@2026", "newPassword": "Updated-test-2026"}, "")
expect(t, 204, status, out)
for _, old := range []string{token, other} {
status, out = f.request("GET", "/auth/me", old, nil, "")
expect(t, 401, status, out)
}
status, out = f.request("POST", "/auth/login", "", map[string]any{"username": "viewer", "password": "Demo@2026"}, "")
expect(t, 401, status, out)
_ = accountLogin(t, f, "viewer", "Updated-test-2026")
var audit string
if err := f.db.Get(&audit, "SELECT action FROM audit_logs WHERE action='change_own_password'"); err != nil {
t.Fatal(err)
}
}
func TestAdministratorValidationRBACAndLastOwner(t *testing.T) {
f := newFixture(t)
admin := f.login("admin")
var adminID string
if err := f.db.Get(&adminID, "SELECT id FROM administrators WHERE username='admin'"); err != nil {
t.Fatal(err)
}
for _, role := range []string{"operator", "viewer"} {
token := f.login(role)
for _, r := range []struct{ method, path string }{{"GET", "/administrators"}, {"POST", "/administrators"}, {"PUT", "/administrators/" + adminID}, {"POST", "/administrators/" + adminID + "/password"}} {
status, out := f.request(r.method, r.path, token, map[string]any{}, "")
expect(t, 403, status, out)
}
}
for _, password := range []string{"short", strings.Repeat("a", 73), strings.Repeat("密", 25)} {
status, out := f.request("POST", "/administrators", admin, map[string]any{"username": "invalid-pass", "displayName": "Invalid", "role": "viewer", "password": password}, "")
expect(t, 400, status, out)
}
body := map[string]any{"username": "test-user", "displayName": "Valid", "role": "super_admin", "password": testAccountPassword}
status, out := f.request("POST", "/administrators", admin, body, "")
expect(t, 201, status, out)
status, out = f.request("POST", "/administrators", admin, body, "")
expect(t, 409, status, out)
status, out = f.request("PUT", "/administrators/"+adminID, admin, map[string]any{"displayName": "Self", "role": "viewer", "enabled": true}, "")
expect(t, 409, status, out)
status, out = f.request("POST", "/administrators/"+adminID+"/password", admin, map[string]any{"password": testAccountPassword}, "")
expect(t, 409, status, out)
status, out = f.request("PUT", "/administrators/"+adminID, admin, map[string]any{"displayName": "Self name", "role": "super_admin", "enabled": true}, "")
expect(t, 200, status, out)
status, out = f.request("GET", "/auth/me", admin, nil, "")
expect(t, 200, status, out)
if out["displayName"] != "Self name" {
t.Fatal(out)
}
// The final owner's role and enabled state remain protected independently of UI restrictions.
if _, err := f.db.Exec("UPDATE administrators SET enabled=false WHERE username='test-user'"); err != nil {
t.Fatal(err)
}
status, out = f.request("PUT", "/administrators/"+adminID, admin, map[string]any{"displayName": "Self", "role": "super_admin", "enabled": false}, "")
expect(t, 409, status, out)
if out["code"] != "last_super_admin" {
t.Fatal(out)
}
}
func TestConcurrentAdministratorDisableKeepsOneOwner(t *testing.T) {
f := newFixture(t)
admin := f.login("admin")
otherID := createTestAccount(t, f, admin, "second-owner", "super_admin")
other := accountLogin(t, f, "second-owner", testAccountPassword)
var adminID string
if err := f.db.Get(&adminID, "SELECT id FROM administrators WHERE username='admin'"); err != nil {
t.Fatal(err)
}
start := make(chan struct{})
results := make(chan int, 2)
var group sync.WaitGroup
for _, r := range []struct{ token, target string }{{admin, otherID}, {other, adminID}} {
group.Add(1)
go func(token, target string) {
defer group.Done()
<-start
status, _ := f.request("PUT", "/administrators/"+target, token, map[string]any{"displayName": "Concurrent", "role": "super_admin", "enabled": false}, "")
results <- status
}(r.token, r.target)
}
close(start)
group.Wait()
close(results)
success := 0
for status := range results {
if status == 200 {
success++
} else if status != 401 && status != 403 {
t.Fatalf("unexpected losing request status %d", status)
}
}
var count int
if err := f.db.Get(&count, "SELECT count(*) FROM administrators WHERE enabled AND role='super_admin'"); err != nil {
t.Fatal(err)
}
if success != 1 || count != 1 {
t.Fatal("concurrent writes lost owner", success, count)
}
}