feat: fix dashboard real-time data sync, stabilize node order and eliminate UI jitter
Release / Build & Release (push) Canceled after 0s
Release / Build & Release (push) Canceled after 0s
This commit is contained in:
@@ -0,0 +1,175 @@
|
||||
package cluster
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/wjyhk/vps-commander/internal/storage"
|
||||
"github.com/wjyhk/vps-commander/internal/sysinfo"
|
||||
)
|
||||
|
||||
func TestLifecycleAcceptance(t *testing.T) {
|
||||
dbPath := filepath.Join(t.TempDir(), "acceptance.db")
|
||||
store, err := storage.Open(dbPath)
|
||||
if err != nil {
|
||||
t.Fatalf("storage.Open failed: %v", err)
|
||||
}
|
||||
defer store.DB.Close()
|
||||
|
||||
secret := "acc-secret"
|
||||
mgr := NewManager(secret, store)
|
||||
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("/agent/ws", mgr.ServeWS)
|
||||
ts := httptest.NewServer(mux)
|
||||
defer ts.Close()
|
||||
|
||||
wsURL := "ws" + ts.URL[4:] + "/agent/ws?device_name=node-test"
|
||||
header := http.Header{"Authorization": []string{"Bearer " + secret}}
|
||||
|
||||
// [DoD 1] 初次注册持久化
|
||||
t.Run("DoD 1: Register and Persist", func(t *testing.T) {
|
||||
c, _, err := websocket.DefaultDialer.Dial(wsURL, header)
|
||||
if err != nil {
|
||||
t.Fatalf("dial failed: %v", err)
|
||||
}
|
||||
defer c.Close()
|
||||
|
||||
prof := sysinfo.Collect("APPLICATION")
|
||||
bProf, _ := json.Marshal(prof)
|
||||
_ = bProf
|
||||
|
||||
msg := Message{
|
||||
Event: "hello",
|
||||
Device: "node-test",
|
||||
Arch: "amd64",
|
||||
OS: "linux",
|
||||
Profile: &prof,
|
||||
TS: time.Now().Unix(),
|
||||
}
|
||||
if err := c.WriteJSON(msg); err != nil {
|
||||
t.Fatalf("writeJSON failed: %v", err)
|
||||
}
|
||||
time.Sleep(150 * time.Millisecond)
|
||||
|
||||
rows, err := store.ListDevices()
|
||||
if err != nil || len(rows) != 1 {
|
||||
t.Fatalf("expected 1 row in DB, got: %d, err: %v", len(rows), err)
|
||||
}
|
||||
if rows[0].Status != "online" || rows[0].Arch != "amd64" || rows[0].OS != "linux" {
|
||||
t.Fatalf("DB device mismatch: %+v", rows[0])
|
||||
}
|
||||
})
|
||||
|
||||
// [DoD 2] 离线状态保留
|
||||
t.Run("DoD 2: Offline Persistence on Disconnect", func(t *testing.T) {
|
||||
// 上个连接已 defer c.Close(),休眠等待触发 readLoop defer
|
||||
time.Sleep(150 * time.Millisecond)
|
||||
|
||||
if mgr.Online("node-test") {
|
||||
t.Fatalf("Online('node-test') should be false after disconnect")
|
||||
}
|
||||
|
||||
devs := mgr.Devices()
|
||||
var target map[string]any
|
||||
for _, d := range devs {
|
||||
if d["name"] == "node-test" {
|
||||
target = d
|
||||
break
|
||||
}
|
||||
}
|
||||
if target == nil {
|
||||
t.Fatalf("device vanished from mgr.Devices() on disconnect!")
|
||||
}
|
||||
if target["status"] != "offline" {
|
||||
t.Fatalf("device status expected 'offline', got: %v", target["status"])
|
||||
}
|
||||
|
||||
rows, _ := store.ListDevices()
|
||||
if len(rows) != 1 || rows[0].Status != "offline" {
|
||||
t.Fatalf("store device status expected 'offline', got: %+v", rows)
|
||||
}
|
||||
})
|
||||
|
||||
// [DoD 3] 离线执行防护
|
||||
t.Run("DoD 3: Offline Exec Guard", func(t *testing.T) {
|
||||
_, err := mgr.Exec(context.Background(), "node-test", ExecPayload{Command: "uname -a"})
|
||||
if err == nil || err.Error() != "device offline" {
|
||||
t.Fatalf("expected 'device offline' error, got: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
// [DoD 4] 自动重连恢复在线
|
||||
t.Run("DoD 4: Reconnect Restore Online", func(t *testing.T) {
|
||||
c, _, err := websocket.DefaultDialer.Dial(wsURL, header)
|
||||
if err != nil {
|
||||
t.Fatalf("dial failed: %v", err)
|
||||
}
|
||||
defer c.Close()
|
||||
|
||||
prof := sysinfo.Collect("APPLICATION")
|
||||
_ = c.WriteJSON(Message{
|
||||
Event: "ping",
|
||||
Device: "node-test",
|
||||
Profile: &prof,
|
||||
TS: time.Now().Unix(),
|
||||
})
|
||||
time.Sleep(150 * time.Millisecond)
|
||||
|
||||
if !mgr.Online("node-test") {
|
||||
t.Fatalf("expected Online('node-test') to be true after reconnect")
|
||||
}
|
||||
devs := mgr.Devices()
|
||||
for _, d := range devs {
|
||||
if d["name"] == "node-test" && d["status"] != "online" {
|
||||
t.Fatalf("expected status 'online', got: %v", d["status"])
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// [DoD 5] Hub 重启数据不丢 (从 DB 恢复)
|
||||
t.Run("DoD 5: Hub Restart Restore from DB", func(t *testing.T) {
|
||||
mgrNew := NewManager(secret, store)
|
||||
devs := mgrNew.Devices()
|
||||
var found map[string]any
|
||||
for _, d := range devs {
|
||||
if d["name"] == "node-test" {
|
||||
found = d
|
||||
break
|
||||
}
|
||||
}
|
||||
if found == nil {
|
||||
t.Fatalf("restarted Hub failed to restore device from DB")
|
||||
}
|
||||
if found["status"] != "offline" {
|
||||
t.Fatalf("restarted Hub initial device status should be offline, got: %v", found["status"])
|
||||
}
|
||||
if found["profile"] == nil {
|
||||
t.Fatalf("restarted Hub should restore profile snapshot")
|
||||
}
|
||||
})
|
||||
|
||||
// [DoD 6] 主动注销清理与黑名单
|
||||
t.Run("DoD 6: Revoke Cleanup and Blacklist", func(t *testing.T) {
|
||||
if err := mgr.RevokeDevice("node-test", "admin action"); err != nil {
|
||||
t.Fatalf("RevokeDevice failed: %v", err)
|
||||
}
|
||||
if len(mgr.Devices()) != 0 {
|
||||
t.Fatalf("expected 0 devices in mgr, got: %d", len(mgr.Devices()))
|
||||
}
|
||||
rows, _ := store.ListDevices()
|
||||
if len(rows) != 0 {
|
||||
t.Fatalf("expected 0 devices in DB, got: %d", len(rows))
|
||||
}
|
||||
revoked, _ := store.IsRevoked("node-test")
|
||||
if !revoked {
|
||||
t.Fatalf("expected device to be recorded in revoked_devices")
|
||||
}
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user