|
@@ -0,0 +1,91 @@
|
|
|
|
|
+package service
|
|
|
|
|
+
|
|
|
|
|
+import (
|
|
|
|
|
+ "testing"
|
|
|
|
|
+ "time"
|
|
|
|
|
+
|
|
|
|
|
+ "github.com/mhsanaei/3x-ui/v3/internal/database"
|
|
|
|
|
+ "github.com/mhsanaei/3x-ui/v3/internal/database/model"
|
|
|
|
|
+ "github.com/mhsanaei/3x-ui/v3/internal/xray"
|
|
|
|
|
+)
|
|
|
|
|
+
|
|
|
|
|
+// A reset of a quota-disabled client must end enabled even when a traffic tick
|
|
|
|
|
+// runs between its steps: enabling before zeroing lets the tick re-disable it.
|
|
|
|
|
+func TestResetTrafficOfDepletedClientSurvivesTickMidReset(t *testing.T) {
|
|
|
|
|
+ resets := []struct {
|
|
|
|
|
+ name string
|
|
|
|
|
+ run func() error
|
|
|
|
|
+ }{
|
|
|
|
|
+ {"single", func() error {
|
|
|
|
|
+ _, err := (&ClientService{}).ResetTrafficByEmail(&InboundService{}, "d@stale")
|
|
|
|
|
+ return err
|
|
|
|
|
+ }},
|
|
|
|
|
+ {"bulk", func() error {
|
|
|
|
|
+ _, err := (&ClientService{}).BulkResetTraffic(&InboundService{}, []string{"d@stale"})
|
|
|
|
|
+ return err
|
|
|
|
|
+ }},
|
|
|
|
|
+ }
|
|
|
|
|
+ for _, reset := range resets {
|
|
|
|
|
+ t.Run(reset.name, func(t *testing.T) {
|
|
|
|
|
+ requireResetSurvivesTick(t, reset.run)
|
|
|
|
|
+ })
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+func requireResetSurvivesTick(t *testing.T, reset func() error) {
|
|
|
|
|
+ t.Helper()
|
|
|
|
|
+ setupBulkDB(t)
|
|
|
|
|
+ clients := []model.Client{{Email: "d@stale", ID: "aaaaaaaa-0000-0000-0000-00000000000d", SubID: "sub-d", Enable: false, TotalGB: 1000}}
|
|
|
|
|
+ ib := mkInbound(t, 23140, model.VLESS, clientsSettings(t, clients))
|
|
|
|
|
+ if err := (&ClientService{}).SyncInbound(nil, ib.Id, clients); err != nil {
|
|
|
|
|
+ t.Fatalf("SyncInbound: %v", err)
|
|
|
|
|
+ }
|
|
|
|
|
+ row := xray.ClientTraffic{InboundId: ib.Id, Email: "d@stale", Enable: false, Up: 600, Down: 400, Total: 1000}
|
|
|
|
|
+ if err := database.GetDB().Create(&row).Error; err != nil {
|
|
|
|
|
+ t.Fatalf("seed client_traffics: %v", err)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ resetTrafficWriterForTest(t)
|
|
|
|
|
+ StartTrafficWriter()
|
|
|
|
|
+ parked := make(chan struct{})
|
|
|
|
|
+ release := make(chan struct{})
|
|
|
|
|
+ go func() {
|
|
|
|
|
+ _ = submitTrafficWrite(func() error {
|
|
|
|
|
+ close(parked)
|
|
|
|
|
+ <-release
|
|
|
|
|
+ return nil
|
|
|
|
|
+ })
|
|
|
|
|
+ }()
|
|
|
|
|
+ <-parked
|
|
|
|
|
+
|
|
|
|
|
+ resetDone := make(chan error, 1)
|
|
|
|
|
+ go func() { resetDone <- reset() }()
|
|
|
|
|
+ waitTrafficWriterQueued(t)
|
|
|
|
|
+ tickDone := make(chan error, 1)
|
|
|
|
|
+ go func() {
|
|
|
|
|
+ _, _, err := (&InboundService{}).AddTraffic(nil, nil)
|
|
|
|
|
+ tickDone <- err
|
|
|
|
|
+ }()
|
|
|
|
|
+ deadline := time.Now().Add(5 * time.Second)
|
|
|
|
|
+ for len(twQueue) < 2 && time.Now().Before(deadline) {
|
|
|
|
|
+ time.Sleep(time.Millisecond)
|
|
|
|
|
+ }
|
|
|
|
|
+ close(release)
|
|
|
|
|
+ if err := <-resetDone; err != nil {
|
|
|
|
|
+ t.Fatalf("reset: %v", err)
|
|
|
|
|
+ }
|
|
|
|
|
+ if err := <-tickDone; err != nil {
|
|
|
|
|
+ t.Fatalf("AddTraffic: %v", err)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ var after xray.ClientTraffic
|
|
|
|
|
+ if err := database.GetDB().Where("email = ?", "d@stale").First(&after).Error; err != nil {
|
|
|
|
|
+ t.Fatalf("read client_traffics: %v", err)
|
|
|
|
|
+ }
|
|
|
|
|
+ c, _ := settingsClient(t, ib.Id, "d@stale")
|
|
|
|
|
+ rec := lookupClientRecord(t, "d@stale")
|
|
|
|
|
+ if !after.Enable || !c.Enable || !rec.Enable || after.Up+after.Down != 0 {
|
|
|
|
|
+ t.Fatalf("after reset: traffic enable=%v used=%d, settings enable=%v, record enable=%v; want all enabled at 0",
|
|
|
|
|
+ after.Enable, after.Up+after.Down, c.Enable, rec.Enable)
|
|
|
|
|
+ }
|
|
|
|
|
+}
|