mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-11-05 20:55:29 +03:00
dbb6212988
Restore usable clients when renewed, avoid duplicate reset updates, and prioritize operator writes in the serialized traffic writer. Preserve manual disables and keep every inbound consistent after traffic resets.
285 lines
9.6 KiB
Go
285 lines
9.6 KiB
Go
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"
|
|
)
|
|
|
|
// renewalReenableDay keeps the seeds one day past (or before) their boundary.
|
|
const renewalReenableDay = int64(24 * 60 * 60 * 1000)
|
|
|
|
// seedRenewableDepletedClient seeds a local inbound whose client the quota loop
|
|
// switched off: record off, inbound JSON off, and the traffic row off and depleted.
|
|
func seedRenewableDepletedClient(t *testing.T, svc *ClientService, port int, email string, total, expiry, up, down int64) *model.Inbound {
|
|
t.Helper()
|
|
ib := seedLocalDisabledClient(t, svc, port, "", email, total, expiry, up, down)
|
|
if got := trafficOf(t, email); got.Enable {
|
|
t.Fatalf("seed %s: want an off traffic row, got %+v", email, got)
|
|
}
|
|
return ib
|
|
}
|
|
|
|
func TestUpdate_ReenablesDepletedClient_WhenExpiryExtended(t *testing.T) {
|
|
setupBulkDB(t)
|
|
svc := &ClientService{}
|
|
inboundSvc := &InboundService{}
|
|
|
|
now := time.Now().UnixMilli()
|
|
email := "renew-exp@x"
|
|
ib := seedRenewableDepletedClient(t, svc, 53101, email, 0, now-2*renewalReenableDay, 0, 0)
|
|
|
|
rec, err := svc.GetRecordByEmail(nil, email)
|
|
if err != nil {
|
|
t.Fatalf("GetRecordByEmail: %v", err)
|
|
}
|
|
updated := rec.ToClient()
|
|
updated.ExpiryTime = now + 30*renewalReenableDay
|
|
// The editor round-trips the switch as the stored off state; the renewal
|
|
// must turn it back on by itself.
|
|
if _, err := svc.Update(inboundSvc, rec.Id, *updated, 0); err != nil {
|
|
t.Fatalf("Update: %v", err)
|
|
}
|
|
assertEnableEverywhere(t, svc, inboundSvc, ib.Id, email, true)
|
|
if got := trafficOf(t, email).ExpiryTime; got != now+30*renewalReenableDay {
|
|
t.Fatalf("%s: expiry = %d, want %d", email, got, now+30*renewalReenableDay)
|
|
}
|
|
}
|
|
|
|
func TestUpdate_ReenablesDepletedClient_WhenTotalRaised(t *testing.T) {
|
|
setupBulkDB(t)
|
|
svc := &ClientService{}
|
|
inboundSvc := &InboundService{}
|
|
|
|
email := "renew-total@x"
|
|
ib := seedRenewableDepletedClient(t, svc, 53102, email, 100, 0, 60, 40)
|
|
|
|
rec, err := svc.GetRecordByEmail(nil, email)
|
|
if err != nil {
|
|
t.Fatalf("GetRecordByEmail: %v", err)
|
|
}
|
|
updated := rec.ToClient()
|
|
updated.TotalGB = 300
|
|
if _, err := svc.Update(inboundSvc, rec.Id, *updated, 0); err != nil {
|
|
t.Fatalf("Update: %v", err)
|
|
}
|
|
assertEnableEverywhere(t, svc, inboundSvc, ib.Id, email, true)
|
|
if got := trafficOf(t, email).Total; got != 300 {
|
|
t.Fatalf("%s: total = %d, want 300", email, got)
|
|
}
|
|
}
|
|
|
|
func TestUpdate_KeepsDisabled_StillExpiredAfterUpdate(t *testing.T) {
|
|
setupBulkDB(t)
|
|
svc := &ClientService{}
|
|
inboundSvc := &InboundService{}
|
|
|
|
now := time.Now().UnixMilli()
|
|
email := "renew-sml@x"
|
|
ib := seedRenewableDepletedClient(t, svc, 53103, email, 0, now-10*renewalReenableDay, 0, 0)
|
|
|
|
rec, err := svc.GetRecordByEmail(nil, email)
|
|
if err != nil {
|
|
t.Fatalf("GetRecordByEmail: %v", err)
|
|
}
|
|
updated := rec.ToClient()
|
|
updated.ExpiryTime = now - 5*renewalReenableDay // an extension that still lands in the past
|
|
if _, err := svc.Update(inboundSvc, rec.Id, *updated, 0); err != nil {
|
|
t.Fatalf("Update: %v", err)
|
|
}
|
|
assertEnableEverywhere(t, svc, inboundSvc, ib.Id, email, false)
|
|
}
|
|
|
|
func TestUpdate_KeepsDisabled_StillOverQuotaAfterUpdate(t *testing.T) {
|
|
setupBulkDB(t)
|
|
svc := &ClientService{}
|
|
inboundSvc := &InboundService{}
|
|
|
|
email := "renew-quota@x"
|
|
ib := seedRenewableDepletedClient(t, svc, 53104, email, 100, 0, 60, 40)
|
|
|
|
rec, err := svc.GetRecordByEmail(nil, email)
|
|
if err != nil {
|
|
t.Fatalf("GetRecordByEmail: %v", err)
|
|
}
|
|
updated := rec.ToClient()
|
|
updated.TotalGB = 80 // still below the 100 bytes of usage
|
|
if _, err := svc.Update(inboundSvc, rec.Id, *updated, 0); err != nil {
|
|
t.Fatalf("Update: %v", err)
|
|
}
|
|
assertEnableEverywhere(t, svc, inboundSvc, ib.Id, email, false)
|
|
}
|
|
|
|
func TestUpdate_KeepsDisabled_ManuallyDisabledNotDepleted(t *testing.T) {
|
|
setupBulkDB(t)
|
|
svc := &ClientService{}
|
|
inboundSvc := &InboundService{}
|
|
|
|
now := time.Now().UnixMilli()
|
|
email := "renew-manual@x"
|
|
ib := seedRenewableDepletedClient(t, svc, 53105, email, 0, now+30*renewalReenableDay, 0, 0)
|
|
|
|
rec, err := svc.GetRecordByEmail(nil, email)
|
|
if err != nil {
|
|
t.Fatalf("GetRecordByEmail: %v", err)
|
|
}
|
|
updated := rec.ToClient()
|
|
updated.ExpiryTime = now + 60*renewalReenableDay
|
|
if _, err := svc.Update(inboundSvc, rec.Id, *updated, 0); err != nil {
|
|
t.Fatalf("Update: %v", err)
|
|
}
|
|
// The quota loop never switched this client off — it reads under quota —
|
|
// so a manual switch-off survives a renewal.
|
|
assertEnableEverywhere(t, svc, inboundSvc, ib.Id, email, false)
|
|
}
|
|
|
|
func TestUpdate_ReenablesDepletedClient_SkipsStillDepletedGlobalUsage(t *testing.T) {
|
|
setupBulkDB(t)
|
|
svc := &ClientService{}
|
|
inboundSvc := &InboundService{}
|
|
|
|
now := time.Now().UnixMilli()
|
|
email := "renew-global@x"
|
|
ib := seedRenewableDepletedClient(t, svc, 53106, email, 100, now+30*renewalReenableDay, 0, 0)
|
|
// A fresh master push depletes the local limits even though up/down read 0.
|
|
global := model.ClientGlobalTraffic{MasterGuid: "master-a", Email: email, Up: 90, Down: 60}
|
|
if err := database.GetDB().Create(&global).Error; err != nil {
|
|
t.Fatalf("seed global traffic: %v", err)
|
|
}
|
|
|
|
rec, err := svc.GetRecordByEmail(nil, email)
|
|
if err != nil {
|
|
t.Fatalf("GetRecordByEmail: %v", err)
|
|
}
|
|
updated := rec.ToClient()
|
|
updated.TotalGB = 100
|
|
if _, err := svc.Update(inboundSvc, rec.Id, *updated, 0); err != nil {
|
|
t.Fatalf("Update: %v", err)
|
|
}
|
|
assertEnableEverywhere(t, svc, inboundSvc, ib.Id, email, false)
|
|
|
|
// Raising the quota above the pushed usage re-enables it.
|
|
updated.TotalGB = 1000
|
|
if _, err := svc.Update(inboundSvc, rec.Id, *updated, 0); err != nil {
|
|
t.Fatalf("Update: %v", err)
|
|
}
|
|
assertEnableEverywhere(t, svc, inboundSvc, ib.Id, email, true)
|
|
}
|
|
|
|
func TestUpdate_ReenablesDepletedClient_WithoutInbound(t *testing.T) {
|
|
setupBulkDB(t)
|
|
svc := &ClientService{}
|
|
inboundSvc := &InboundService{}
|
|
|
|
now := time.Now().UnixMilli()
|
|
email := "renew-noib@x"
|
|
rec := &model.ClientRecord{
|
|
Email: email,
|
|
UUID: "44444444-4444-4444-4444-444444444444",
|
|
SubID: email,
|
|
}
|
|
if err := database.GetDB().Create(rec).Error; err != nil {
|
|
t.Fatalf("create record: %v", err)
|
|
}
|
|
forceRecordDisabled(t, svc, email)
|
|
if err := database.GetDB().Create(&xray.ClientTraffic{
|
|
Email: email, Enable: false, ExpiryTime: now - 2*renewalReenableDay,
|
|
}).Error; err != nil {
|
|
t.Fatalf("seed client_traffics: %v", err)
|
|
}
|
|
|
|
updated := rec.ToClient()
|
|
updated.ExpiryTime = now + 30*renewalReenableDay
|
|
if _, err := svc.Update(inboundSvc, rec.Id, *updated, 0); err != nil {
|
|
t.Fatalf("Update: %v", err)
|
|
}
|
|
if got := recordEnableOf(t, svc, email); !got {
|
|
t.Fatalf("%s: client_records.enable = false, want true", email)
|
|
}
|
|
if got := trafficOf(t, email).Enable; !got {
|
|
t.Fatalf("%s: client_traffics.enable = false, want true (no-inbound re-enable gap)", email)
|
|
}
|
|
}
|
|
|
|
func TestResetClientExpiryTimeByEmail_ReenablesDepletedClient(t *testing.T) {
|
|
setupBulkDB(t)
|
|
svc := &ClientService{}
|
|
inboundSvc := &InboundService{}
|
|
|
|
now := time.Now().UnixMilli()
|
|
email := "renew-setter@x"
|
|
ib := seedRenewableDepletedClient(t, svc, 53107, email, 0, now-2*renewalReenableDay, 0, 0)
|
|
|
|
if _, err := svc.ResetClientExpiryTimeByEmail(inboundSvc, email, now+30*renewalReenableDay); err != nil {
|
|
t.Fatalf("ResetClientExpiryTimeByEmail: %v", err)
|
|
}
|
|
assertEnableEverywhere(t, svc, inboundSvc, ib.Id, email, true)
|
|
}
|
|
|
|
func TestResetClientExpiryTimeByEmail_KeepsDisabled_ManuallyDisabledNotDepleted(t *testing.T) {
|
|
setupBulkDB(t)
|
|
svc := &ClientService{}
|
|
inboundSvc := &InboundService{}
|
|
|
|
now := time.Now().UnixMilli()
|
|
email := "renew-setter-off@x"
|
|
ib := seedRenewableDepletedClient(t, svc, 53108, email, 0, now+30*renewalReenableDay, 0, 0)
|
|
|
|
if _, err := svc.ResetClientExpiryTimeByEmail(inboundSvc, email, now+60*renewalReenableDay); err != nil {
|
|
t.Fatalf("ResetClientExpiryTimeByEmail: %v", err)
|
|
}
|
|
assertEnableEverywhere(t, svc, inboundSvc, ib.Id, email, false)
|
|
}
|
|
|
|
func TestResetClientTrafficLimitByEmail_ReenablesDepletedClient(t *testing.T) {
|
|
setupBulkDB(t)
|
|
svc := &ClientService{}
|
|
inboundSvc := &InboundService{}
|
|
|
|
email := "renew-setter-total@x"
|
|
ib := seedRenewableDepletedClient(t, svc, 53109, email, 100, 0, 60, 40)
|
|
|
|
if _, err := svc.ResetClientTrafficLimitByEmail(inboundSvc, email, 1); err != nil {
|
|
t.Fatalf("ResetClientTrafficLimitByEmail: %v", err)
|
|
}
|
|
assertEnableEverywhere(t, svc, inboundSvc, ib.Id, email, true)
|
|
if got := trafficOf(t, email).Total; got != 1<<30 {
|
|
t.Fatalf("%s: total = %d, want %d", email, got, 1<<30)
|
|
}
|
|
}
|
|
|
|
// A client living on several inbounds must come back on every one of them,
|
|
// not only the inbound whose reset ran first.
|
|
func TestResetTrafficByEmail_ReenablesEveryInboundOfTheClient(t *testing.T) {
|
|
setupBulkDB(t)
|
|
svc := &ClientService{}
|
|
inboundSvc := &InboundService{}
|
|
|
|
email := "renew-multi@x"
|
|
c := model.Client{
|
|
Email: email, ID: "11111111-1111-1111-1111-111111111111", SubID: email,
|
|
Enable: false, TotalGB: 1000,
|
|
}
|
|
ibA := mkInbound(t, 53201, model.VLESS, clientsSettings(t, []model.Client{c}))
|
|
ibB := mkInbound(t, 53202, model.VLESS, clientsSettings(t, []model.Client{c}))
|
|
for _, ib := range []*model.Inbound{ibA, ibB} {
|
|
if err := svc.SyncInbound(nil, ib.Id, []model.Client{c}); err != nil {
|
|
t.Fatalf("SyncInbound %d: %v", ib.Id, err)
|
|
}
|
|
}
|
|
mkTraffic(t, ibA.Id, email, 600, 500, 1000, 0, false)
|
|
forceRecordDisabled(t, svc, email)
|
|
|
|
if _, err := svc.ResetTrafficByEmail(inboundSvc, email); err != nil {
|
|
t.Fatalf("ResetTrafficByEmail: %v", err)
|
|
}
|
|
assertEnableEverywhere(t, svc, inboundSvc, ibA.Id, email, true)
|
|
assertEnableEverywhere(t, svc, inboundSvc, ibB.Id, email, true)
|
|
if got := trafficOf(t, email); !got.Enable || got.Up+got.Down != 0 {
|
|
t.Fatalf("%s: traffic row enable=%v used=%d, want enabled at 0", email, got.Enable, got.Up+got.Down)
|
|
}
|
|
}
|