package server import ( "bytes" "context" "encoding/json" "fmt" "github.com/jmoiron/sqlx" "net/http" "net/http/httptest" "os" "strings" "testing" "time" "vdi-cloud/platform/internal/config" "vdi-cloud/platform/internal/model" "vdi-cloud/platform/internal/store" "vdi-cloud/platform/pkg/proxmox" ) type fixture struct { server *Server db *sqlx.DB t *testing.T } func newFixture(t *testing.T) *fixture { t.Helper() url := os.Getenv("VDI_TEST_DATABASE_URL") if url == "" { t.Skip("set VDI_TEST_DATABASE_URL to a dedicated empty test database") } ctx := context.Background() db, err := store.Open(ctx, url) if err != nil { t.Fatal(err) } if _, err = db.Exec(`TRUNCATE connection_tickets,audit_logs,tasks,bindings,users,terminals,desktops,resources,admin_sessions,administrators CASCADE`); err != nil { t.Fatal(err) } if err = store.Bootstrap(ctx, db, "demo", "", ""); err != nil { t.Fatal(err) } s := New(config.Config{Mode: "demo", JWTSecret: strings.Repeat("test-", 10)}, db, nil) ctx, cancel := context.WithCancel(ctx) go s.RunWorker(ctx) t.Cleanup(func() { cancel(); time.Sleep(30 * time.Millisecond); db.Close() }) return &fixture{s, db, t} } func (f *fixture) request(method, path, token string, body any, key string) (int, map[string]any) { f.t.Helper() var encoded []byte if body != nil { encoded, _ = json.Marshal(body) } r := httptest.NewRequest(method, "/api/v1"+path, bytes.NewReader(encoded)) r.Header.Set("Content-Type", "application/json") if token != "" { r.Header.Set("Authorization", "Bearer "+token) } if key != "" { r.Header.Set("Idempotency-Key", key) } w := httptest.NewRecorder() f.server.Router.ServeHTTP(w, r) out := map[string]any{} if w.Body.Len() > 0 { if err := json.Unmarshal(w.Body.Bytes(), &out); err != nil { f.t.Fatal(err) } } return w.Code, out } func (f *fixture) login(user string) string { f.t.Helper() status, out := f.request("POST", "/auth/login", "", map[string]any{"username": user, "password": "Demo@2026"}, "") if status != 200 { f.t.Fatalf("login failed %d %v", status, out) } return out["accessToken"].(string) } func expect(t *testing.T, want, got int, out any) { t.Helper() if want != got { t.Fatalf("want status %d got %d: %v", want, got, out) } } func TestRBACLogoutAndDurableTask(t *testing.T) { f := newFixture(t) viewer := f.login("viewer") operator := f.login("operator") admin := f.login("admin") status, out := f.request("GET", "/desktops", viewer, nil, "") expect(t, 200, status, out) status, out = f.request("POST", "/desktops/desk_demo_kylin/actions", viewer, map[string]any{"action": "start"}, "viewer-1") expect(t, 403, status, out) status, out = f.request("PUT", "/users/user_demo_wang/bindings", operator, map[string]any{"desktopIds": []string{}}, "") expect(t, 403, status, out) status, out = f.request("POST", "/desktops/desk_demo_kylin/actions", operator, map[string]any{"action": "start"}, "start-1") expect(t, 202, status, out) id := out["id"].(string) status, again := f.request("POST", "/desktops/desk_demo_kylin/actions", operator, map[string]any{"action": "start"}, "start-1") expect(t, 202, status, again) if again["id"] != id { t.Fatal("duplicate task created") } status, out = f.request("POST", "/desktops/desk_demo_kylin/actions", operator, map[string]any{"action": "shutdown"}, "start-1") expect(t, 409, status, out) deadline := time.Now().Add(5 * time.Second) for { status, out = f.request("GET", "/tasks/"+id, admin, nil, "") expect(t, 200, status, out) if out["status"] == "success" { break } if time.Now().After(deadline) { t.Fatal("task never completed", out) } time.Sleep(50 * time.Millisecond) } var power, service string if err := f.db.QueryRow("SELECT power_state,service_state FROM desktops WHERE id='desk_demo_kylin'").Scan(&power, &service); err != nil { t.Fatal(err) } if power != "running" || service != "unknown" { t.Fatal("power and service state confused") } status, out = f.request("POST", "/auth/logout", admin, nil, "") expect(t, 204, status, out) status, out = f.request("GET", "/auth/me", admin, nil, "") expect(t, 401, status, out) var count int f.db.Get(&count, "SELECT count(*) FROM tasks WHERE id=$1", id) if count != 1 { t.Fatal("task not persisted") } } func TestDeviceIdentityBindingTicketAndAcknowledgement(t *testing.T) { f := newFixture(t) admin := f.login("admin") enroll := func(sn string) (string, string) { status, out := f.request("POST", "/terminals", admin, map[string]any{"sn": sn, "name": sn, "group": "测试"}, "") expect(t, 201, status, out) secret := out["activationSecret"].(string) id := out["id"].(string) status, out = f.request("POST", "/terminal/register", "", map[string]any{"sn": sn, "activationSecret": secret, "agentVersion": "test"}, "") expect(t, 200, status, out) token := out["accessToken"].(string) status, out = f.request("POST", "/terminal/register", "", map[string]any{"sn": sn, "activationSecret": secret, "agentVersion": "test"}, "") expect(t, 401, status, out) return id, token } id, device := enroll("TEST-DEVICE-001") otherID, other := enroll("TEST-DEVICE-002") status, out := f.request("GET", "/users", device, nil, "") expect(t, 401, status, out) status, out = f.request("POST", "/terminal/connect-ticket", device, map[string]any{"desktopId": "desk_demo_windows"}, "") expect(t, 403, status, out) status, out = f.request("PUT", "/users/user_demo_wang/bindings", admin, map[string]any{"terminalId": id, "desktopIds": []string{"desk_demo_windows", "desk_demo_ubuntu"}, "enabled": true}, "") expect(t, 200, status, out) status, out = f.request("GET", "/terminal/desktops", device, nil, "") expect(t, 200, status, out) if len(out["items"].([]any)) != 2 { t.Fatal("missing allowed desktops") } f.db.Exec("UPDATE desktops SET endpoint='192.0.2.100' WHERE id='desk_demo_windows'") 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", "/terminal/connect-ticket/consume", other, map[string]any{"ticket": ticket}, "") expect(t, 401, status, out) status, out = f.request("POST", "/terminal/connect-ticket/consume", device, map[string]any{"ticket": ticket}, "") expect(t, 200, status, out) if out["credentialMode"] != "existing-pairing" { t.Fatal("must declare existing Sunshine pairing") } sessionID := out["sessionId"].(string) status, out = f.request("GET", "/overview", admin, nil, "") expect(t, 200, status, out) if out["activeSessions"] != float64(0) { t.Fatal("reservation counted as active stream") } status, out = f.request("POST", "/terminal/session", device, map[string]any{"sessionId": sessionID, "state": "connected"}, "") expect(t, 200, status, out) status, out = f.request("GET", "/overview", admin, nil, "") if out["activeSessions"] != float64(1) { t.Fatal("stream-ready event missing") } status, out = f.request("POST", "/terminal/session", other, map[string]any{"sessionId": sessionID, "state": "ended"}, "") expect(t, 409, status, out) status, out = f.request("POST", "/terminal/session", device, map[string]any{"sessionId": sessionID, "state": "ended"}, "") expect(t, 200, status, out) status, out = f.request("POST", "/terminal/session", device, map[string]any{"sessionId": sessionID, "state": "connected"}, "") expect(t, 409, 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("PUT", "/users/user_demo_wang/bindings", admin, map[string]any{"terminalId": id, "desktopIds": []string{}, "enabled": true}, "") expect(t, 200, status, out) status, out = f.request("POST", "/terminal/connect-ticket", device, map[string]any{"desktopId": "desk_demo_windows"}, "") expect(t, 403, status, out) status, out = f.request("POST", fmt.Sprintf("/terminals/%s/commands", id), admin, map[string]any{"action": "collect-logs"}, "cmd-1") expect(t, 202, status, out) taskID := out["id"].(string) status, out = f.request("GET", "/terminal/commands", other, nil, "") expect(t, 200, status, out) if len(out["items"].([]any)) != 0 { t.Fatal("command leaked to other terminal") } status, out = f.request("POST", "/terminal/commands/"+taskID+"/ack", other, map[string]any{"status": "success"}, "") expect(t, 404, status, out) status, out = f.request("GET", "/terminal/commands", device, nil, "") expect(t, 200, status, out) if len(out["items"].([]any)) != 1 { t.Fatal("device did not receive command") } status, out = f.request("POST", "/terminal/commands/"+taskID+"/ack", device, map[string]any{"status": "failed", "message": "private device log"}, "") expect(t, 200, status, out) status, out = f.request("GET", "/tasks/"+taskID, admin, nil, "") expect(t, 200, status, out) if out["status"] != "failed" { t.Fatal("must wait for device ack") } if strings.Contains(out["message"].(string), "private") { t.Fatal("raw device detail leaked") } status, out = f.request("POST", "/terminals/"+id+"/commands", admin, map[string]any{"action": "collect-logs"}, "diagnostics-2") expect(t, 202, status, out) diagnosticID := out["id"].(string) status, out = f.request("GET", "/terminal/commands", device, nil, "") expect(t, 200, status, out) status, out = f.request("POST", "/terminal/commands/"+diagnosticID+"/ack", device, map[string]any{"status": "success"}, "") expect(t, 400, status, out) status, out = f.request("POST", "/terminal/commands/"+diagnosticID+"/ack", device, map[string]any{"status": "success", "diagnostics": map[string]any{"agentVersion": "qt-test", "bootId": "test-boot", "kioskState": "DESKTOP_LIST", "uptimeSeconds": 100, "logTail": "private diagnostic"}}, "") expect(t, 200, status, out) viewer := f.login("viewer") status, out = f.request("GET", "/tasks/"+diagnosticID+"/diagnostics", viewer, nil, "") expect(t, 403, status, out) status, out = f.request("GET", "/tasks/"+diagnosticID+"/diagnostics", admin, nil, "") expect(t, 200, status, out) if out["logTail"] != "private diagnostic" { t.Fatal("diagnostic not retained") } _ = otherID } func TestUnknownTerminalOutcomeAndBindingRollback(t *testing.T) { f := newFixture(t) admin := f.login("admin") status, out := f.request("PUT", "/users/user_demo_wang/bindings", admin, map[string]any{"terminalId": "term_demo_001", "desktopIds": []string{"missing"}}, "") expect(t, 400, status, out) var n int f.db.Get(&n, "SELECT count(*) FROM bindings WHERE user_id='user_demo_wang'") if n != 2 { t.Fatal("failed binding update lost original authorization") } status, out = f.request("POST", "/terminals/term_demo_001/commands", admin, map[string]any{"action": "reboot"}, "offline-1") expect(t, 202, status, out) id := out["id"].(string) f.db.Exec("UPDATE tasks SET created_at=now()-interval '3 minutes' WHERE id=$1", id) deadline := time.Now().Add(3 * time.Second) for { status, out = f.request("GET", "/tasks/"+id, admin, nil, "") expect(t, 200, status, out) if out["status"] == "timeout" { break } if time.Now().After(deadline) { t.Fatal("offline task was not timed out", out) } time.Sleep(50 * time.Millisecond) } status, out = f.request(http.MethodPost, "/resources/sync", admin, nil, "sync-1") expect(t, 503, status, out) } func TestRestartPreservesRevokedBindingsAndRejectsModeChange(t *testing.T) { f := newFixture(t) if _, err := f.db.Exec("DELETE FROM bindings WHERE user_id='user_demo_wang'"); err != nil { t.Fatal(err) } if err := store.Bootstrap(context.Background(), f.db, "demo", "", ""); err != nil { t.Fatal(err) } var count int if err := f.db.Get(&count, "SELECT count(*) FROM bindings WHERE user_id='user_demo_wang'"); err != nil || count != 0 { t.Fatalf("restart restored revoked bindings: count=%d err=%v", count, err) } if err := store.Bootstrap(context.Background(), f.db, "production", "prod-admin", "long-password-2026"); err == nil { t.Fatal("demo database accepted production bootstrap") } } func TestInitialPasswordPolicyDoesNotRejectExistingAccountOnRestart(t *testing.T) { f := newFixture(t) if _, err := f.db.Exec("UPDATE platform_metadata SET mode='production'"); err != nil { t.Fatal(err) } defer f.db.Exec("UPDATE platform_metadata SET mode='demo'") ctx := context.Background() if err := store.Bootstrap(ctx, f.db, "production", "new-admin", "short"); err == nil { t.Fatal("weak initial password accepted") } if err := store.Bootstrap(ctx, f.db, "production", "new-admin", "strong-initial-password"); err != nil { t.Fatal(err) } var before, after string if err := f.db.Get(&before, "SELECT password_hash FROM administrators WHERE username='new-admin'"); err != nil { t.Fatal(err) } if err := store.Bootstrap(ctx, f.db, "production", "new-admin", "short"); err != nil { t.Fatal("existing account blocked startup", err) } if err := f.db.Get(&after, "SELECT password_hash FROM administrators WHERE username='new-admin'"); err != nil { t.Fatal(err) } if before != after { t.Fatal("restart changed the account password") } } func TestLateCommandAcknowledgementPreservesNewSession(t *testing.T) { f := newFixture(t) if _, err := f.db.Exec("UPDATE terminals SET session_id='new-session',session_state='connected',session_desktop_id='desk_demo_windows' WHERE id='term_demo_001'"); err != nil { t.Fatal(err) } var task model.Task if err := f.db.Get(&task, `INSERT INTO tasks(id,kind,action,target_id,target_name,actor,status,message,idempotency_key) VALUES('task-late','terminal','reset-session','term_demo_001','test','admin','running','','late') RETURNING *`); err != nil { t.Fatal(err) } if err := f.server.complete(context.Background(), task, "success", "ack"); err != nil { t.Fatal(err) } var session string if err := f.db.Get(&session, "SELECT session_id FROM terminals WHERE id='term_demo_001'"); err != nil || session != "new-session" { t.Fatalf("new session lost: %q %v", session, err) } } func TestPVEConfirmedFailureAndUncertainSubmission(t *testing.T) { f := newFixture(t) if _, err := f.db.Exec("UPDATE desktops SET provider='proxmox' WHERE id='desk_demo_kylin'"); err != nil { t.Fatal(err) } for _, tc := range []struct { name string submission int exit string want string }{{"submission unavailable", 503, "", "timeout"}, {"permission denied", 403, "", "failed"}, {"explicit PVE task failure", 200, "ERROR", "failed"}} { t.Run(tc.name, func(t *testing.T) { pve := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") if r.Method == http.MethodPost { w.WriteHeader(tc.submission) fmt.Fprint(w, `{"data":"UPID:test"}`) return } fmt.Fprintf(w, `{"data":{"status":"stopped","exitstatus":%q}}`, tc.exit) })) defer pve.Close() f.server.PVE = &proxmox.Client{BaseURL: pve.URL, HTTP: pve.Client()} var task model.Task if err := f.db.Get(&task, `INSERT INTO tasks(id,kind,action,target_id,target_name,actor,status,message,idempotency_key) VALUES($1,'desktop','start','desk_demo_kylin','test VM','admin','running','test',$1) RETURNING *`, store.ID("task_")); err != nil { t.Fatal(err) } f.server.execute(context.Background(), task) var status string if err := f.db.Get(&status, "SELECT status FROM tasks WHERE id=$1", task.ID); err != nil || status != tc.want { t.Fatalf("want %s, got %s err=%v", tc.want, status, err) } }) } }