Files
xboard-node/internal/controlplane/mailbox_test.go
T

165 lines
4.0 KiB
Go

package controlplane
import (
"testing"
"github.com/cedar2025/xboard-node/internal/model"
)
func TestNodeMailboxLatestStateAndNotifyCollapse(t *testing.T) {
mb := NewNodeMailbox()
cfg1 := &model.NodeSpec{Protocol: "vmess", ServerPort: 10010}
cfg2 := &model.NodeSpec{Protocol: "vless", ServerPort: 10020}
mb.Apply(Event{Type: EventSyncConfig, Config: cfg1})
mb.Apply(Event{Type: EventSyncConfig, Config: cfg2})
select {
case <-mb.NotifyCh():
default:
t.Fatal("expected collapsed notify signal")
}
mb.MarkReady()
state := mb.DrainIfReady()
if !state.HasConfig || state.Config == nil {
t.Fatal("expected config state")
}
if state.Config.Protocol != "vless" || state.Config.ServerPort != 10020 {
t.Fatalf("unexpected config: %+v", state.Config)
}
}
func TestNodeMailboxFullUsersOverwriteAndDeltaBeforeBaseline(t *testing.T) {
mb := NewNodeMailbox()
mb.Apply(Event{
Type: EventSyncUserDelta,
DeltaAction: "add",
DeltaUsers: []model.UserSpec{{ID: 1, UUID: "u1"}},
})
mb.MarkReady()
state := mb.DrainIfReady()
if !state.NeedsReconcile {
t.Fatal("expected reconcile when delta arrives before baseline")
}
mb.Apply(Event{Type: EventSyncUsers, Users: []model.UserSpec{{ID: 1, UUID: "u1"}}})
mb.Apply(Event{Type: EventSyncUsers, Users: []model.UserSpec{{ID: 2, UUID: "u2"}}})
state = mb.DrainIfReady()
if !state.HasUsers {
t.Fatal("expected users snapshot")
}
if len(state.Users) != 1 || state.Users[0].ID != 2 {
t.Fatalf("unexpected users: %+v", state.Users)
}
}
func TestNodeMailboxDevicesLatestWins(t *testing.T) {
mb := NewNodeMailbox()
mb.Apply(Event{Type: EventSyncDevices, DeviceUsers: map[int][]string{1: {"1.1.1.1"}}})
mb.Apply(Event{Type: EventSyncDevices, DeviceUsers: map[int][]string{1: {"2.2.2.2"}}})
mb.MarkReady()
state := mb.DrainIfReady()
if !state.HasDevices {
t.Fatal("expected devices state")
}
if got := state.DeviceUsers[1][0]; got != "2.2.2.2" {
t.Fatalf("unexpected device state: %v", state.DeviceUsers)
}
}
func TestNodeMailboxReconcileNotifies(t *testing.T) {
mb := NewNodeMailbox()
mb.MarkReady()
// Drain the MarkReady notification
select {
case <-mb.NotifyCh():
default:
}
// Delta without baseline should trigger reconcile AND notify
mb.Apply(Event{
Type: EventSyncUserDelta,
DeltaAction: "add",
DeltaUsers: []model.UserSpec{{ID: 1, UUID: "u1"}},
})
select {
case <-mb.NotifyCh():
default:
t.Fatal("expected notify on reconcile path")
}
state := mb.DrainIfReady()
if !state.NeedsReconcile {
t.Fatal("expected NeedsReconcile")
}
}
func TestNodeMailboxSeedBaselineEnablesDelta(t *testing.T) {
mb := NewNodeMailbox()
mb.SeedBaseline([]model.UserSpec{{ID: 1, UUID: "u1"}, {ID: 2, UUID: "u2"}}, nil)
mb.MarkReady()
// Drain MarkReady notify
select {
case <-mb.NotifyCh():
default:
}
// Delta should now apply incrementally, not trigger reconcile
mb.Apply(Event{
Type: EventSyncUserDelta,
DeltaAction: "add",
DeltaUsers: []model.UserSpec{{ID: 3, UUID: "u3"}},
})
select {
case <-mb.NotifyCh():
default:
t.Fatal("expected notify after delta")
}
state := mb.DrainIfReady()
if state.NeedsReconcile {
t.Fatal("should not need reconcile after seeded baseline")
}
if !state.HasUsers {
t.Fatal("expected users")
}
if len(state.Users) != 3 {
t.Fatalf("expected 3 users, got %d: %+v", len(state.Users), state.Users)
}
}
func TestNodeMailboxDeltaFailedApplyNotifies(t *testing.T) {
mb := NewNodeMailbox()
mb.SeedBaseline([]model.UserSpec{{ID: 1, UUID: "u1"}}, nil)
mb.MarkReady()
// Drain MarkReady notify
select {
case <-mb.NotifyCh():
default:
}
// Invalid action should trigger reconcile and still notify
mb.Apply(Event{
Type: EventSyncUserDelta,
DeltaAction: "invalid_action",
DeltaUsers: []model.UserSpec{{ID: 2, UUID: "u2"}},
})
select {
case <-mb.NotifyCh():
default:
t.Fatal("expected notify on failed delta apply")
}
state := mb.DrainIfReady()
if !state.NeedsReconcile {
t.Fatal("expected NeedsReconcile for invalid delta action")
}
}