221 lines
11 KiB
Go
221 lines
11 KiB
Go
package server
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"strings"
|
|
"testing"
|
|
"vdi-cloud/platform/internal/store"
|
|
)
|
|
|
|
func lifecycleDevice(t *testing.T, f *fixture, admin, sn string) (string, string, string) {
|
|
t.Helper()
|
|
status, out := f.request("POST", "/terminals", admin, map[string]any{"sn": sn, "name": "测试设备", "group": "原分组"}, "")
|
|
expect(t, 201, status, out)
|
|
id, secret := out["id"].(string), out["activationSecret"].(string)
|
|
status, out = f.request("POST", "/terminal/register", "", map[string]any{"sn": sn, "activationSecret": secret, "agentVersion": "test"}, "")
|
|
expect(t, 200, status, out)
|
|
return id, out["accessToken"].(string), secret
|
|
}
|
|
func lifecycleBind(t *testing.T, f *fixture, admin, id string) {
|
|
t.Helper()
|
|
if _, err := f.db.Exec("UPDATE desktops SET endpoint='192.0.2.100',power_state='running' WHERE id='desk_demo_windows'"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
status, out := f.request("PUT", "/users/user_demo_wang/bindings", admin, map[string]any{"terminalId": id, "desktopIds": []string{"desk_demo_windows"}, "enabled": true}, "")
|
|
expect(t, 200, status, out)
|
|
}
|
|
|
|
func TestTerminalLifecycleRevokesIdentityAndTickets(t *testing.T) {
|
|
f := newFixture(t)
|
|
admin := f.login("admin")
|
|
id, device, originalSecret := lifecycleDevice(t, f, admin, "LIFECYCLE-001")
|
|
lifecycleBind(t, f, admin, id)
|
|
status, out := f.request("PUT", "/terminals/"+id, admin, map[string]any{"name": " 新设备名 ", "group": " 财务 "}, "")
|
|
expect(t, 200, status, out)
|
|
status, out = f.request("POST", "/terminal/connect-ticket", device, map[string]any{"desktopId": "desk_demo_windows"}, "")
|
|
expect(t, 201, status, out)
|
|
ticket := out["ticket"].(string)
|
|
status, out = f.request("POST", "/terminals/"+id+"/disable", admin, nil, "")
|
|
expect(t, 200, status, out)
|
|
for _, path := range []string{"/terminal/config", "/terminal/desktops", "/terminal/commands"} {
|
|
status, out = f.request("GET", path, device, nil, "")
|
|
expect(t, 401, status, out)
|
|
}
|
|
status, out = f.request("POST", "/terminal/heartbeat", device, map[string]any{"agentVersion": "stale"}, "")
|
|
expect(t, 401, status, out)
|
|
status, out = f.request("POST", "/terminal/register", "", map[string]any{"sn": "LIFECYCLE-001", "activationSecret": originalSecret, "agentVersion": "stale"}, "")
|
|
expect(t, 401, status, out)
|
|
status, out = f.request("POST", "/terminals/"+id+"/commands", admin, map[string]any{"action": "collect-logs"}, "disabled-command")
|
|
expect(t, 409, status, out)
|
|
status, out = f.request("GET", "/terminals", admin, nil, "")
|
|
expect(t, 200, status, out)
|
|
var found bool
|
|
for _, raw := range out["items"].([]any) {
|
|
item := raw.(map[string]any)
|
|
if item["id"] == id {
|
|
found = true
|
|
if item["status"] != "disabled" || item["enabled"] != false || item["name"] != "新设备名" || item["group"] != "财务" {
|
|
t.Fatal("incorrect terminal state", item)
|
|
}
|
|
}
|
|
}
|
|
if !found {
|
|
t.Fatal("terminal disappeared")
|
|
}
|
|
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 terminals WHERE id=$1", id); err != nil || enabled {
|
|
t.Fatal("restart enabled disabled terminal", err)
|
|
}
|
|
status, out = f.request("POST", "/terminals/"+id+"/reactivate", admin, nil, "")
|
|
expect(t, 200, status, out)
|
|
firstSecret := out["activationSecret"].(string)
|
|
status, out = f.request("POST", "/terminals/"+id+"/reactivate", admin, nil, "")
|
|
expect(t, 200, status, out)
|
|
secret := out["activationSecret"].(string)
|
|
if secret == firstSecret || secret == originalSecret {
|
|
t.Fatal("reactivation reused credentials")
|
|
}
|
|
status, out = f.request("POST", "/terminal/register", "", map[string]any{"sn": "LIFECYCLE-001", "activationSecret": firstSecret, "agentVersion": "test"}, "")
|
|
expect(t, 401, status, out)
|
|
status, out = f.request("POST", "/terminal/register", "", map[string]any{"sn": "LIFECYCLE-001", "activationSecret": secret, "agentVersion": "test"}, "")
|
|
expect(t, 200, status, out)
|
|
newDevice := out["accessToken"].(string)
|
|
status, out = f.request("POST", "/terminal/register", "", map[string]any{"sn": "LIFECYCLE-001", "activationSecret": secret, "agentVersion": "test"}, "")
|
|
expect(t, 401, status, out)
|
|
status, out = f.request("POST", "/terminal/connect-ticket/consume", newDevice, map[string]any{"ticket": ticket}, "")
|
|
expect(t, 401, status, out)
|
|
status, out = f.request("GET", "/terminal/desktops", newDevice, nil, "")
|
|
expect(t, 200, status, out)
|
|
if len(out["items"].([]any)) != 1 {
|
|
t.Fatal("reactivation lost authorization")
|
|
}
|
|
status, out = f.request("GET", "/terminal/config", device, nil, "")
|
|
expect(t, 401, status, out)
|
|
status, out = f.request("GET", "/audit-logs", admin, nil, "")
|
|
expect(t, 200, status, out)
|
|
audit, _ := json.Marshal(out)
|
|
if strings.Contains(string(audit), secret) || strings.Contains(string(audit), newDevice) || !strings.Contains(string(audit), "reactivate_terminal") {
|
|
t.Fatal("audit missing or leaked credentials")
|
|
}
|
|
}
|
|
|
|
func TestTerminalLifecycleGuardsSessionsTasksAndRoles(t *testing.T) {
|
|
f := newFixture(t)
|
|
admin := f.login("admin")
|
|
id, device, _ := lifecycleDevice(t, f, admin, "LIFECYCLE-002")
|
|
lifecycleBind(t, f, admin, id)
|
|
for _, role := range []string{"viewer", "operator"} {
|
|
token := f.login(role)
|
|
for _, path := range []string{"/disable", "/reactivate"} {
|
|
status, out := f.request("POST", "/terminals/"+id+path, token, nil, "")
|
|
expect(t, 403, status, out)
|
|
}
|
|
status, out := f.request("PUT", "/terminals/"+id, token, map[string]any{"name": "无权限", "group": ""}, "")
|
|
expect(t, 403, status, out)
|
|
status, out = f.request("PUT", "/users/user_demo_wang", token, map[string]any{"displayName": "无权限", "enabled": false}, "")
|
|
expect(t, 403, status, out)
|
|
status, out = f.request("POST", "/users/batch-bindings", token, map[string]any{"userIds": []string{"user_demo_wang"}, "desktopIds": []string{"desk_demo_windows"}, "action": "revoke"}, "")
|
|
expect(t, 403, status, out)
|
|
}
|
|
status, out := f.request("POST", "/terminal/connect-ticket", device, map[string]any{"desktopId": "desk_demo_windows"}, "")
|
|
expect(t, 201, status, out)
|
|
status, out = f.request("POST", "/terminal/connect-ticket/consume", device, map[string]any{"ticket": out["ticket"]}, "")
|
|
expect(t, 200, status, out)
|
|
session := out["sessionId"].(string)
|
|
for _, path := range []string{"/disable", "/reactivate"} {
|
|
status, out = f.request("POST", "/terminals/"+id+path, admin, nil, "")
|
|
expect(t, 409, status, out)
|
|
if out["code"] != "terminal_in_use" {
|
|
t.Fatal(out)
|
|
}
|
|
}
|
|
status, out = f.request("POST", "/terminal/session", device, map[string]any{"sessionId": session, "state": "ended"}, "")
|
|
expect(t, 200, status, out)
|
|
status, out = f.request("POST", "/terminals/"+id+"/commands", admin, map[string]any{"action": "collect-logs"}, "pending-command")
|
|
expect(t, 202, status, out)
|
|
task := out["id"].(string)
|
|
for _, path := range []string{"/disable", "/reactivate"} {
|
|
status, out = f.request("POST", "/terminals/"+id+path, admin, nil, "")
|
|
expect(t, 409, status, out)
|
|
if out["code"] != "terminal_task_pending" {
|
|
t.Fatal(out)
|
|
}
|
|
}
|
|
status, out = f.request("GET", "/terminal/commands", device, nil, "")
|
|
expect(t, 200, status, out)
|
|
status, out = f.request("POST", "/terminal/commands/"+task+"/ack", device, map[string]any{"status": "failed"}, "")
|
|
expect(t, 200, status, out)
|
|
status, out = f.request("POST", "/terminals/"+id+"/disable", admin, nil, "")
|
|
expect(t, 200, status, out)
|
|
status, out = f.request("PUT", "/terminals/"+id, admin, map[string]any{"name": " ", "group": ""}, "")
|
|
expect(t, 400, status, out)
|
|
status, out = f.request("POST", "/terminals/missing/reactivate", admin, nil, "")
|
|
expect(t, 404, status, out)
|
|
}
|
|
|
|
func TestUserMaintenanceAndBatchBindingsAreAtomic(t *testing.T) {
|
|
f := newFixture(t)
|
|
admin := f.login("admin")
|
|
id, device, _ := lifecycleDevice(t, f, admin, "LIFECYCLE-003")
|
|
lifecycleBind(t, f, admin, id)
|
|
status, out := f.request("POST", "/terminal/connect-ticket", device, map[string]any{"desktopId": "desk_demo_windows"}, "")
|
|
expect(t, 201, status, out)
|
|
ticket := out["ticket"]
|
|
status, out = f.request("PUT", "/users/user_demo_wang", admin, map[string]any{"displayName": " 新姓名 ", "enabled": false}, "")
|
|
expect(t, 200, status, out)
|
|
if out["displayName"] != "新姓名" || out["username"] != "wang.demo" || len(out["desktopIds"].([]any)) != 1 {
|
|
t.Fatal("profile update changed authorization", out)
|
|
}
|
|
status, out = f.request("GET", "/terminal/desktops", device, nil, "")
|
|
expect(t, 200, status, out)
|
|
if len(out["items"].([]any)) != 0 {
|
|
t.Fatal("disabled user still authorized")
|
|
}
|
|
status, out = f.request("PUT", "/users/user_demo_wang", admin, map[string]any{"displayName": "新姓名", "enabled": true}, "")
|
|
expect(t, 200, status, out)
|
|
status, out = f.request("POST", "/terminal/connect-ticket/consume", device, map[string]any{"ticket": ticket}, "")
|
|
expect(t, 401, status, out)
|
|
status, out = f.request("POST", "/users", admin, map[string]any{"username": "batch-other", "displayName": "其他用户"}, "")
|
|
expect(t, 201, status, out)
|
|
other := out["id"].(string)
|
|
body := map[string]any{"userIds": []string{"user_demo_wang", other, other}, "desktopIds": []string{"desk_demo_ubuntu", "desk_demo_ubuntu"}, "action": "grant"}
|
|
for i := 0; i < 2; i++ {
|
|
status, out = f.request("POST", "/users/batch-bindings", admin, body, "")
|
|
expect(t, 200, status, out)
|
|
if out["userCount"] != float64(2) {
|
|
t.Fatal(out)
|
|
}
|
|
}
|
|
var count int
|
|
if err := f.db.Get(&count, "SELECT count(*) FROM bindings WHERE user_id='user_demo_wang'"); err != nil || count != 2 {
|
|
t.Fatal("grant replaced existing grants", count, err)
|
|
}
|
|
body["desktopIds"] = []string{"desk_demo_ubuntu", "missing"}
|
|
body["action"] = "revoke"
|
|
status, out = f.request("POST", "/users/batch-bindings", admin, body, "")
|
|
expect(t, 400, status, out)
|
|
f.db.Get(&count, "SELECT count(*) FROM bindings WHERE user_id='user_demo_wang'")
|
|
if count != 2 {
|
|
t.Fatal("invalid batch partially committed")
|
|
}
|
|
body["desktopIds"] = []string{"desk_demo_ubuntu"}
|
|
body["userIds"] = []string{"user_demo_wang", "missing"}
|
|
status, out = f.request("POST", "/users/batch-bindings", admin, body, "")
|
|
expect(t, 400, status, out)
|
|
body["userIds"] = []string{"user_demo_wang", other}
|
|
status, out = f.request("POST", "/users/batch-bindings", admin, body, "")
|
|
expect(t, 200, status, out)
|
|
f.db.Get(&count, "SELECT count(*) FROM bindings WHERE user_id='user_demo_wang'")
|
|
if count != 1 {
|
|
t.Fatal("revoke removed unrelated desktop")
|
|
}
|
|
status, out = f.request("POST", "/users/batch-bindings", admin, map[string]any{"userIds": []string{}, "desktopIds": []string{}, "action": "grant"}, "")
|
|
expect(t, 400, status, out)
|
|
status, out = f.request("PUT", "/users/user_demo_wang", admin, map[string]any{"displayName": " ", "enabled": true}, "")
|
|
expect(t, 400, status, out)
|
|
}
|