reconcile_skip_test.go 2.5 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465
  1. package runtime
  2. import (
  3. "context"
  4. "net/http"
  5. "net/http/httptest"
  6. "strings"
  7. "sync/atomic"
  8. "testing"
  9. "github.com/mhsanaei/3x-ui/v3/internal/database/model"
  10. )
  11. // TestReconcileInbound_SkipsUnchanged proves the delta-skip: a second reconcile
  12. // of an unchanged inbound that the node still reports sends no push, while a
  13. // content change or an absent-on-node inbound forces a fresh push.
  14. func TestReconcileInbound_SkipsUnchanged(t *testing.T) {
  15. var pushes atomic.Int32
  16. srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  17. if r.Method == http.MethodPost && strings.Contains(r.URL.Path, "/panel/api/inbounds/update/") {
  18. pushes.Add(1)
  19. }
  20. w.Header().Set("Content-Type", "application/json")
  21. _, _ = w.Write([]byte(`{"success":true}`))
  22. }))
  23. defer srv.Close()
  24. r := NewRemote(nodeForPlainServer(t, srv, "verify", "tok"), nil)
  25. ib := &model.Inbound{Tag: "in-1", Protocol: model.VLESS, Port: 443, Settings: `{"clients":[]}`}
  26. // Pre-seed the tag→id cache so resolveRemoteID needs no network round-trip.
  27. r.cacheSet(ib.Tag, 7)
  28. // First reconcile: node doesn't report it yet → must push and record the fp.
  29. if pushed, err := r.ReconcileInbound(context.Background(), ib, false); err != nil || !pushed {
  30. t.Fatalf("first reconcile: pushed=%v err=%v, want push", pushed, err)
  31. }
  32. if got := pushes.Load(); got != 1 {
  33. t.Fatalf("after first reconcile pushes=%d, want 1", got)
  34. }
  35. // Second reconcile: unchanged and present on node → skip.
  36. if pushed, err := r.ReconcileInbound(context.Background(), ib, true); err != nil || pushed {
  37. t.Fatalf("second reconcile: pushed=%v err=%v, want skip", pushed, err)
  38. }
  39. if got := pushes.Load(); got != 1 {
  40. t.Fatalf("unchanged reconcile pushed again: pushes=%d, want 1", got)
  41. }
  42. // Content change → push again even though it's present on node.
  43. ib.Settings = `{"clients":[{"email":"a@x"}]}`
  44. if pushed, err := r.ReconcileInbound(context.Background(), ib, true); err != nil || !pushed {
  45. t.Fatalf("changed reconcile: pushed=%v err=%v, want push", pushed, err)
  46. }
  47. if got := pushes.Load(); got != 2 {
  48. t.Fatalf("changed reconcile pushes=%d, want 2", got)
  49. }
  50. // Absent on node (e.g. node restarted/lost it) → re-push even if fp matches.
  51. if pushed, err := r.ReconcileInbound(context.Background(), ib, false); err != nil || !pushed {
  52. t.Fatalf("absent-on-node reconcile: pushed=%v err=%v, want push", pushed, err)
  53. }
  54. if got := pushes.Load(); got != 3 {
  55. t.Fatalf("absent-on-node reconcile pushes=%d, want 3", got)
  56. }
  57. }