| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146 |
- package service
- import (
- "testing"
- "github.com/mhsanaei/3x-ui/v3/internal/database"
- "github.com/mhsanaei/3x-ui/v3/internal/database/model"
- "github.com/mhsanaei/3x-ui/v3/internal/web/runtime"
- )
- // While a node is config-dirty (a local edit committed before it could be
- // mirrored to the node), the traffic pull must not overwrite the central
- // inbound's config columns from the node's stale snapshot — only traffic
- // counters may advance. Otherwise a reconnecting node reverts the edit.
- func TestSetRemoteTraffic_DirtyPreservesConfig(t *testing.T) {
- setupConflictDB(t)
- db := database.GetDB()
- node := &model.Node{Name: "n1", Address: "127.0.0.1", Port: 2096, ApiToken: "tok", Enable: true, Status: "online"}
- if err := db.Create(node).Error; err != nil {
- t.Fatalf("create node: %v", err)
- }
- id := node.Id
- const desiredSettings = `{"clients":[{"email":"a@x"}]}`
- central := &model.Inbound{
- UserId: 1,
- NodeID: &id,
- Tag: "in-443-tcp",
- Enable: true,
- Port: 443,
- Protocol: model.VLESS,
- Settings: desiredSettings,
- }
- if err := db.Create(central).Error; err != nil {
- t.Fatalf("create inbound: %v", err)
- }
- snap := &runtime.TrafficSnapshot{
- Inbounds: []*model.Inbound{{
- Tag: "in-443-tcp",
- Enable: true,
- Port: 443,
- Protocol: model.VLESS,
- Settings: `{"clients":[{"email":"b@x"}]}`,
- Up: 500,
- Down: 700,
- }},
- }
- svc := InboundService{}
- if _, err := svc.setRemoteTrafficLocked(id, snap, true); err != nil {
- t.Fatalf("setRemoteTrafficLocked dirty: %v", err)
- }
- var got model.Inbound
- if err := db.First(&got, central.Id).Error; err != nil {
- t.Fatalf("reload inbound: %v", err)
- }
- if got.Settings != desiredSettings {
- t.Fatalf("dirty pull overwrote settings: want %q got %q", desiredSettings, got.Settings)
- }
- if got.Up != 500 || got.Down != 700 {
- t.Fatalf("traffic counters not applied while dirty: up=%d down=%d", got.Up, got.Down)
- }
- }
- // Deleting a *disabled* client attached to a node inbound must still propagate
- // to the node. The node's own DB carries the (disabled) client, so the central
- // panel has to mark the node dirty (→ reconcile) instead of dropping the delete
- // and letting the next traffic snapshot resurrect the client. Regression for
- // the enable-flag gate that used to skip the node path entirely (#5352).
- func TestDelInboundClientByEmail_DisabledNodeClientMarksDirty(t *testing.T) {
- setupConflictDB(t)
- db := database.GetDB()
- // Offline node so nodePushPlan reports dirty without needing a live runtime.
- node := &model.Node{Name: "n1", Address: "127.0.0.1", Port: 2096, ApiToken: "tok", Enable: true, Status: "offline"}
- if err := db.Create(node).Error; err != nil {
- t.Fatalf("create node: %v", err)
- }
- id := node.Id
- central := &model.Inbound{
- UserId: 1,
- NodeID: &id,
- Tag: "in-443-tcp",
- Enable: true,
- Port: 443,
- Protocol: model.VLESS,
- Settings: `{"clients":[{"email":"a@x","enable":false}]}`,
- }
- if err := db.Create(central).Error; err != nil {
- t.Fatalf("create inbound: %v", err)
- }
- inboundSvc := &InboundService{}
- clientSvc := &ClientService{}
- if _, err := clientSvc.DelInboundClientByEmail(inboundSvc, central.Id, "a@x", false); err != nil {
- t.Fatalf("DelInboundClientByEmail: %v", err)
- }
- if _, _, dirty, _, err := (&NodeService{}).NodeSyncState(id); err != nil {
- t.Fatalf("NodeSyncState: %v", err)
- } else if !dirty {
- t.Fatal("deleting a disabled node client must mark the node dirty (#5352)")
- }
- }
- // ClearNodeDirty must be a compare-and-swap on config_dirty_at so a concurrent
- // edit that re-dirties the node during a reconcile is not silently cleared.
- func TestNodeDirty_ClearIsCASOnDirtyAt(t *testing.T) {
- setupConflictDB(t)
- db := database.GetDB()
- node := &model.Node{Name: "n2", Address: "127.0.0.1", Port: 2096, ApiToken: "tok", Enable: true, Status: "online"}
- if err := db.Create(node).Error; err != nil {
- t.Fatalf("create node: %v", err)
- }
- nodeSvc := NodeService{}
- if err := nodeSvc.MarkNodeDirty(node.Id); err != nil {
- t.Fatalf("MarkNodeDirty: %v", err)
- }
- _, _, dirty, dirtyAt, err := nodeSvc.NodeSyncState(node.Id)
- if err != nil {
- t.Fatalf("NodeSyncState: %v", err)
- }
- if !dirty {
- t.Fatal("node should be dirty after MarkNodeDirty")
- }
- if err := nodeSvc.ClearNodeDirty(node.Id, dirtyAt-1); err != nil {
- t.Fatalf("ClearNodeDirty stale token: %v", err)
- }
- if _, _, stillDirty, _, _ := nodeSvc.NodeSyncState(node.Id); !stillDirty {
- t.Fatal("stale-token clear must not clear the dirty flag")
- }
- if err := nodeSvc.ClearNodeDirty(node.Id, dirtyAt); err != nil {
- t.Fatalf("ClearNodeDirty matching token: %v", err)
- }
- if _, _, stillDirty, _, _ := nodeSvc.NodeSyncState(node.Id); stillDirty {
- t.Fatal("matching-token clear must clear the dirty flag")
- }
- }
|