3x-ui/web/service/node_sync_test.go

239 lines
6.6 KiB
Go

package service
import (
"path/filepath"
"testing"
"github.com/mhsanaei/3x-ui/v2/database"
"github.com/mhsanaei/3x-ui/v2/database/model"
)
func writeNodeSyncSettings(t *testing.T, nodeID string) {
t.Helper()
writeNodeGuardSettings(t, map[string]any{
"dbType": "mariadb",
"nodeRole": "worker",
"nodeId": nodeID,
})
}
func loadNodeState(t *testing.T, nodeID string) *model.NodeState {
t.Helper()
state := &model.NodeState{}
if err := database.GetDB().First(state, "node_id = ?", nodeID).Error; err != nil {
t.Fatalf("load node state error: %v", err)
}
return state
}
func TestLoadAndSaveSharedAccountsSnapshot(t *testing.T) {
setupTestDB(t)
cachePath := filepath.Join(t.TempDir(), "shared-cache.json")
snapshot := &SharedAccountsSnapshot{
Version: 7,
Inbounds: []*model.Inbound{
{
Id: 11,
Enable: true,
Port: 443,
Protocol: model.VLESS,
Settings: `{"clients":[{"id":"u-1","email":"alice@example.com"}]}`,
},
},
}
if err := SaveSharedAccountsSnapshot(cachePath, snapshot); err != nil {
t.Fatalf("SaveSharedAccountsSnapshot error: %v", err)
}
loaded, err := LoadSharedAccountsSnapshot(cachePath)
if err != nil {
t.Fatalf("LoadSharedAccountsSnapshot error: %v", err)
}
if loaded.Version != snapshot.Version {
t.Fatalf("expected version %d, got %d", snapshot.Version, loaded.Version)
}
if len(loaded.Inbounds) != 1 || loaded.Inbounds[0].Tag != snapshot.Inbounds[0].Tag {
t.Fatalf("expected one inbound to round-trip")
}
}
func TestSyncOnceSkipsApplyWhenVersionUnchanged(t *testing.T) {
setupTestDB(t)
writeNodeSyncSettings(t, "worker-skip")
cachePath := filepath.Join(t.TempDir(), "shared-cache.json")
applyCalled := false
loadSnapshotCalled := false
syncSvc := &NodeSyncService{
cachePath: cachePath,
lastSeenVersion: 9,
loadVersion: func() (int64, error) { return 9, nil },
loadSnapshot: func() (*SharedAccountsSnapshot, error) { loadSnapshotCalled = true; return nil, nil },
applySnapshot: func(*SharedAccountsSnapshot) error { applyCalled = true; return nil },
}
didSync, err := syncSvc.SyncOnce()
if err != nil {
t.Fatalf("SyncOnce error: %v", err)
}
if didSync {
t.Fatal("expected unchanged version to skip sync")
}
if loadSnapshotCalled {
t.Fatal("loadSnapshot should not be called when version is unchanged")
}
if applyCalled {
t.Fatal("applySnapshot should not be called when version is unchanged")
}
state := loadNodeState(t, "worker-skip")
if state.LastSeenVersion != 9 {
t.Fatalf("expected last seen version 9, got %d", state.LastSeenVersion)
}
if state.LastSyncAt != 0 {
t.Fatalf("expected LastSyncAt to remain 0, got %d", state.LastSyncAt)
}
if state.LastHeartbeatAt == 0 {
t.Fatal("expected heartbeat timestamp to be recorded")
}
}
func TestSyncOnceRefreshesCacheAndAppliesSnapshot(t *testing.T) {
setupTestDB(t)
writeNodeSyncSettings(t, "worker-refresh")
cachePath := filepath.Join(t.TempDir(), "shared-cache.json")
wantSnapshot := &SharedAccountsSnapshot{
Version: 12,
Inbounds: []*model.Inbound{
{
Id: 100,
Enable: true,
Port: 8443,
Protocol: model.VLESS,
Settings: `{"clients":[]}`,
StreamSettings: `{"network":"tcp","tlsSettings":{"settings":{"allowInsecure":true}}}`,
Tag: "in-100",
},
},
}
applyCalls := 0
syncSvc := &NodeSyncService{
cachePath: cachePath,
lastSeenVersion: 11,
loadVersion: func() (int64, error) { return 12, nil },
loadSnapshot: func() (*SharedAccountsSnapshot, error) { return wantSnapshot, nil },
applySnapshot: func(snapshot *SharedAccountsSnapshot) error { applyCalls++; return nil },
}
didSync, err := syncSvc.SyncOnce()
if err != nil {
t.Fatalf("SyncOnce error: %v", err)
}
if !didSync {
t.Fatal("expected sync to run when version changes")
}
if applyCalls != 1 {
t.Fatalf("expected applySnapshot to be called once, got %d", applyCalls)
}
if syncSvc.lastSeenVersion != 12 {
t.Fatalf("expected lastSeenVersion to become 12, got %d", syncSvc.lastSeenVersion)
}
cached, err := LoadSharedAccountsSnapshot(cachePath)
if err != nil {
t.Fatalf("LoadSharedAccountsSnapshot error: %v", err)
}
if cached.Version != 12 {
t.Fatalf("expected cached version 12, got %d", cached.Version)
}
state := loadNodeState(t, "worker-refresh")
if state.LastSeenVersion != 12 {
t.Fatalf("expected last seen version 12, got %d", state.LastSeenVersion)
}
if state.LastSyncAt == 0 {
t.Fatal("expected LastSyncAt to be recorded after successful sync")
}
if state.LastError != "" {
t.Fatalf("expected empty LastError, got %q", state.LastError)
}
}
func TestSyncOncePreservesLastSyncAtWhenVersionUnchanged(t *testing.T) {
setupTestDB(t)
writeNodeSyncSettings(t, "worker-preserve")
if err := database.UpsertNodeState(database.GetDB(), &model.NodeState{
NodeID: "worker-preserve",
NodeRole: "worker",
LastSyncAt: 12345,
LastHeartbeatAt: 12345,
LastSeenVersion: 8,
}); err != nil {
t.Fatalf("UpsertNodeState error: %v", err)
}
syncSvc := &NodeSyncService{
cachePath: filepath.Join(t.TempDir(), "shared-cache.json"),
lastSeenVersion: 8,
loadVersion: func() (int64, error) { return 8, nil },
loadSnapshot: func() (*SharedAccountsSnapshot, error) { return nil, nil },
applySnapshot: func(*SharedAccountsSnapshot) error { return nil },
}
didSync, err := syncSvc.SyncOnce()
if err != nil {
t.Fatalf("SyncOnce error: %v", err)
}
if didSync {
t.Fatal("expected unchanged version to skip sync")
}
state := loadNodeState(t, "worker-preserve")
if state.LastSyncAt != 12345 {
t.Fatalf("expected LastSyncAt to remain 12345, got %d", state.LastSyncAt)
}
}
func TestBootstrapFromCacheAppliesCachedSnapshot(t *testing.T) {
setupTestDB(t)
writeNodeSyncSettings(t, "worker-bootstrap")
cachePath := filepath.Join(t.TempDir(), "shared-cache.json")
cached := &SharedAccountsSnapshot{
Version: 77,
Inbounds: []*model.Inbound{
{
Id: 77,
Enable: true,
Port: 10077,
Protocol: model.VLESS,
Settings: `{"clients":[]}`,
Tag: "cache-77",
},
},
}
if err := SaveSharedAccountsSnapshot(cachePath, cached); err != nil {
t.Fatalf("SaveSharedAccountsSnapshot error: %v", err)
}
appliedVersion := int64(0)
syncSvc := &NodeSyncService{
cachePath: cachePath,
applySnapshot: func(snapshot *SharedAccountsSnapshot) error {
appliedVersion = snapshot.Version
return nil
},
}
if err := syncSvc.BootstrapFromCache(); err != nil {
t.Fatalf("BootstrapFromCache error: %v", err)
}
if appliedVersion != 77 {
t.Fatalf("expected cached version 77 to be applied, got %d", appliedVersion)
}
}