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