mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-11-02 03:05:02 +03:00
7f64a67ba5
Every traffic reset re-enabled its clients unconditionally, in the client record and in the traffic row a master treats as the client's state. A master forwards an inbound's scheduled reset and "reset all" to its nodes through the per-client reset calls, so a client disabled by hand on a node inbound came back on at the next scheduled reset. Each reset now decides, before it zeroes the counters, which of its clients the quota switched off, counting cross-panel usage, and re-enables only those; a client disabled below its quota keeps its record and traffic row off. The reset API docs say so.
336 lines
11 KiB
Go
336 lines
11 KiB
Go
package service
|
|
|
|
import (
|
|
"fmt"
|
|
"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"
|
|
)
|
|
|
|
func mkTraffic(t *testing.T, inboundId int, email string, up, down, total, expiry int64, enable bool) {
|
|
t.Helper()
|
|
row := xray.ClientTraffic{
|
|
InboundId: inboundId,
|
|
Email: email,
|
|
Up: up,
|
|
Down: down,
|
|
Total: total,
|
|
ExpiryTime: expiry,
|
|
Enable: enable,
|
|
}
|
|
if err := database.GetDB().Create(&row).Error; err != nil {
|
|
t.Fatalf("create traffic %s: %v", email, err)
|
|
}
|
|
}
|
|
|
|
func trafficOf(t *testing.T, email string) xray.ClientTraffic {
|
|
t.Helper()
|
|
var row xray.ClientTraffic
|
|
if err := database.GetDB().Where("email = ?", email).First(&row).Error; err != nil {
|
|
t.Fatalf("load traffic %s: %v", email, err)
|
|
}
|
|
return row
|
|
}
|
|
|
|
func TestBulkResetTrafficZeroesUsageAndReenables(t *testing.T) {
|
|
setupBulkDB(t)
|
|
svc := &ClientService{}
|
|
inboundSvc := &InboundService{}
|
|
|
|
source := []model.Client{
|
|
{Email: "alice@x", ID: "11111111-1111-1111-1111-111111111111", SubID: "sa", Enable: true},
|
|
{Email: "bob@x", ID: "22222222-2222-2222-2222-222222222222", SubID: "sb", Enable: true},
|
|
{Email: "carol@x", ID: "33333333-3333-3333-3333-333333333333", SubID: "sc", Enable: true},
|
|
}
|
|
ib := mkInbound(t, 21001, model.VLESS, clientsSettings(t, source))
|
|
if err := svc.SyncInbound(nil, ib.Id, source); err != nil {
|
|
t.Fatalf("seed linkage: %v", err)
|
|
}
|
|
mkTraffic(t, ib.Id, "alice@x", 10, 20, 30, 0, false)
|
|
mkTraffic(t, ib.Id, "bob@x", 5, 5, 0, 0, true)
|
|
mkTraffic(t, ib.Id, "carol@x", 7, 0, 0, 0, true)
|
|
|
|
affected, err := svc.BulkResetTraffic(inboundSvc, []string{"alice@x", "bob@x"})
|
|
if err != nil {
|
|
t.Fatalf("BulkResetTraffic: %v", err)
|
|
}
|
|
if affected != 2 {
|
|
t.Fatalf("expected 2 affected, got %d", affected)
|
|
}
|
|
|
|
for _, e := range []string{"alice@x", "bob@x"} {
|
|
tr := trafficOf(t, e)
|
|
if tr.Up != 0 || tr.Down != 0 {
|
|
t.Fatalf("%s: expected up/down 0, got up=%d down=%d", e, tr.Up, tr.Down)
|
|
}
|
|
if !tr.Enable {
|
|
t.Fatalf("%s: expected re-enabled", e)
|
|
}
|
|
}
|
|
|
|
carol := trafficOf(t, "carol@x")
|
|
if carol.Up != 7 {
|
|
t.Fatalf("carol not in list should be untouched, got up=%d", carol.Up)
|
|
}
|
|
}
|
|
|
|
func TestDelDepletedRemovesOnlyDepleted(t *testing.T) {
|
|
setupBulkDB(t)
|
|
svc := &ClientService{}
|
|
inboundSvc := &InboundService{}
|
|
|
|
source := []model.Client{
|
|
{Email: "alice@x", ID: "11111111-1111-1111-1111-111111111111", SubID: "sa", Enable: true},
|
|
{Email: "bob@x", ID: "22222222-2222-2222-2222-222222222222", SubID: "sb", Enable: true},
|
|
{Email: "carol@x", ID: "33333333-3333-3333-3333-333333333333", SubID: "sc", Enable: true},
|
|
}
|
|
ib := mkInbound(t, 21002, model.VLESS, clientsSettings(t, source))
|
|
if err := svc.SyncInbound(nil, ib.Id, source); err != nil {
|
|
t.Fatalf("seed linkage: %v", err)
|
|
}
|
|
past := time.Now().Add(-time.Hour).UnixMilli()
|
|
mkTraffic(t, ib.Id, "alice@x", 60, 60, 100, 0, true)
|
|
mkTraffic(t, ib.Id, "bob@x", 10, 10, 100, 0, true)
|
|
mkTraffic(t, ib.Id, "carol@x", 0, 0, 0, past, true)
|
|
|
|
deleted, _, err := svc.DelDepleted(inboundSvc)
|
|
if err != nil {
|
|
t.Fatalf("DelDepleted: %v", err)
|
|
}
|
|
if deleted != 2 {
|
|
t.Fatalf("expected 2 deleted (alice traffic-depleted, carol expired), got %d", deleted)
|
|
}
|
|
|
|
if _, err := svc.GetRecordByEmail(nil, "bob@x"); err != nil {
|
|
t.Fatalf("bob should survive: %v", err)
|
|
}
|
|
for _, e := range []string{"alice@x", "carol@x"} {
|
|
if _, err := svc.GetRecordByEmail(nil, e); err == nil {
|
|
t.Fatalf("%s should be deleted", e)
|
|
}
|
|
}
|
|
|
|
reloaded, _ := inboundSvc.GetInbound(ib.Id)
|
|
jsonClients, _ := inboundSvc.GetClients(reloaded)
|
|
if len(jsonClients) != 1 || jsonClients[0].Email != "bob@x" {
|
|
t.Fatalf("settings JSON should contain only bob, got %d clients", len(jsonClients))
|
|
}
|
|
}
|
|
|
|
func TestClientDeleteKeepTrafficPreservesRowForAttachedClient(t *testing.T) {
|
|
setupBulkDB(t)
|
|
svc := &ClientService{}
|
|
inboundSvc := &InboundService{}
|
|
|
|
email := "keepme@x"
|
|
c := model.Client{Email: email, ID: "11111111-1111-1111-1111-111111111111", SubID: email, Enable: true}
|
|
ib := mkInbound(t, 52030, model.VLESS, clientsSettings(t, []model.Client{c}))
|
|
if err := svc.SyncInbound(nil, ib.Id, []model.Client{c}); err != nil {
|
|
t.Fatalf("seed linkage: %v", err)
|
|
}
|
|
mkTraffic(t, ib.Id, email, 111, 222, 0, 0, true)
|
|
|
|
rec := lookupClientRecord(t, email)
|
|
if _, err := svc.Delete(inboundSvc, rec.Id, true); err != nil {
|
|
t.Fatalf("Delete(keepTraffic): %v", err)
|
|
}
|
|
|
|
var cnt int64
|
|
if err := database.GetDB().Model(&xray.ClientTraffic{}).Where("email = ?", email).Count(&cnt).Error; err != nil {
|
|
t.Fatalf("count traffic: %v", err)
|
|
}
|
|
if cnt != 1 {
|
|
t.Fatalf("keepTraffic delete of an inbound-attached client must preserve its client_traffics row, found %d", cnt)
|
|
}
|
|
}
|
|
|
|
func TestBulkDeleteRemovesClientExternalLinks(t *testing.T) {
|
|
setupBulkDB(t)
|
|
svc := &ClientService{}
|
|
inboundSvc := &InboundService{}
|
|
|
|
email := "extlink@x"
|
|
c := model.Client{Email: email, ID: "11111111-1111-1111-1111-111111111111", SubID: email, Enable: true}
|
|
ib := mkInbound(t, 52040, model.VLESS, clientsSettings(t, []model.Client{c}))
|
|
if err := svc.SyncInbound(nil, ib.Id, []model.Client{c}); err != nil {
|
|
t.Fatalf("seed linkage: %v", err)
|
|
}
|
|
rec := lookupClientRecord(t, email)
|
|
if err := database.GetDB().Create(&model.ClientExternalLink{ClientId: rec.Id, Kind: "sub", Value: "https://example.com/x"}).Error; err != nil {
|
|
t.Fatalf("seed external link: %v", err)
|
|
}
|
|
|
|
if _, _, err := svc.BulkDelete(inboundSvc, []string{email}, false); err != nil {
|
|
t.Fatalf("BulkDelete: %v", err)
|
|
}
|
|
|
|
var cnt int64
|
|
if err := database.GetDB().Model(&model.ClientExternalLink{}).Where("client_id = ?", rec.Id).Count(&cnt).Error; err != nil {
|
|
t.Fatalf("count external links: %v", err)
|
|
}
|
|
if cnt != 0 {
|
|
t.Fatalf("BulkDelete left %d orphan external-link row(s) behind", cnt)
|
|
}
|
|
}
|
|
|
|
func TestGetClientTrafficByEmailReadsClientsTable(t *testing.T) {
|
|
setupBulkDB(t)
|
|
svc := &ClientService{}
|
|
inboundSvc := &InboundService{}
|
|
|
|
source := []model.Client{
|
|
{Email: "alice@x", ID: "11111111-1111-1111-1111-111111111111", SubID: "sa", Enable: true},
|
|
}
|
|
ib := mkInbound(t, 21003, model.VLESS, clientsSettings(t, source))
|
|
if err := svc.SyncInbound(nil, ib.Id, source); err != nil {
|
|
t.Fatalf("seed linkage: %v", err)
|
|
}
|
|
mkTraffic(t, ib.Id, "alice@x", 1, 2, 0, 0, true)
|
|
|
|
tr, err := inboundSvc.GetClientTrafficByEmail("alice@x")
|
|
if err != nil {
|
|
t.Fatalf("GetClientTrafficByEmail: %v", err)
|
|
}
|
|
if tr == nil {
|
|
t.Fatalf("expected traffic, got nil")
|
|
return
|
|
}
|
|
if tr.UUID != "11111111-1111-1111-1111-111111111111" {
|
|
t.Fatalf("UUID not enriched from clients table, got %q", tr.UUID)
|
|
}
|
|
if tr.SubId != "sa" {
|
|
t.Fatalf("SubId not enriched from clients table, got %q", tr.SubId)
|
|
}
|
|
}
|
|
|
|
// A depleted usage row keyed by a padded email belongs to no client; trimming it on the
|
|
// way to BulkDelete deleted the healthy client it resembles.
|
|
func TestDelDepletedIgnoresPaddedUsageRow(t *testing.T) {
|
|
setupBulkDB(t)
|
|
svc := &ClientService{}
|
|
inboundSvc := &InboundService{}
|
|
|
|
source := []model.Client{{Email: "alice@x", ID: "11111111-1111-1111-1111-111111111111", SubID: "sa", Enable: true}}
|
|
ib := mkInbound(t, 21003, model.VLESS, clientsSettings(t, source))
|
|
if err := svc.SyncInbound(nil, ib.Id, source); err != nil {
|
|
t.Fatalf("seed linkage: %v", err)
|
|
}
|
|
mkTraffic(t, ib.Id, "alice@x", 0, 0, 0, 0, true)
|
|
mkTraffic(t, ib.Id, "alice@x ", 0, 0, 0, 1, false)
|
|
|
|
deleted, _, err := svc.DelDepleted(inboundSvc)
|
|
if err != nil {
|
|
t.Fatalf("DelDepleted: %v", err)
|
|
}
|
|
if deleted != 0 {
|
|
t.Fatalf("deleted %d clients, want 0", deleted)
|
|
}
|
|
if _, err := svc.GetRecordByEmail(nil, "alice@x"); err != nil {
|
|
t.Fatalf("alice was deleted through a look-alike usage row: %v", err)
|
|
}
|
|
}
|
|
|
|
// trafficResets is every way to reset a client's traffic; a master forwards its
|
|
// scheduled and "reset all" resets to a node through the first two.
|
|
var trafficResets = []struct {
|
|
name string
|
|
run func(ib *model.Inbound, email string) error
|
|
}{
|
|
{"ResetTrafficByEmail", func(_ *model.Inbound, email string) error {
|
|
_, err := (&ClientService{}).ResetTrafficByEmail(&InboundService{}, email)
|
|
return err
|
|
}},
|
|
{"BulkResetTraffic", func(_ *model.Inbound, email string) error {
|
|
_, err := (&ClientService{}).BulkResetTraffic(&InboundService{}, []string{email})
|
|
return err
|
|
}},
|
|
{"ResetAllClientTraffics", func(ib *model.Inbound, _ string) error {
|
|
_, err := (&ClientService{}).ResetAllClientTraffics(&InboundService{}, ib.Id)
|
|
return err
|
|
}},
|
|
{"ResetAllTraffics", func(*model.Inbound, string) error {
|
|
_, err := (&ClientService{}).ResetAllTraffics()
|
|
return err
|
|
}},
|
|
{"ResetClientTraffic", func(ib *model.Inbound, email string) error {
|
|
_, err := (&InboundService{}).ResetClientTraffic(ib.Id, email)
|
|
return err
|
|
}},
|
|
{"ResetClientTrafficByEmail", func(_ *model.Inbound, email string) error {
|
|
return (&InboundService{}).ResetClientTrafficByEmail(email)
|
|
}},
|
|
}
|
|
|
|
func TestTrafficReset_LeavesOperatorDisabledClientOff(t *testing.T) {
|
|
const email = "banned@x"
|
|
for _, total := range []int64{0, 1000} {
|
|
for _, reset := range trafficResets {
|
|
t.Run(fmt.Sprintf("%s/quota=%d", reset.name, total), func(t *testing.T) {
|
|
setupBulkDB(t)
|
|
svc := &ClientService{}
|
|
// Disabled with usage below quota: nothing but a human did that.
|
|
ib := seedLocalDisabledClient(t, svc, 52020, "", email, total, 0, 10, 10)
|
|
|
|
if err := reset.run(ib, email); err != nil {
|
|
t.Fatalf("%s: %v", reset.name, err)
|
|
}
|
|
|
|
assertEnableEverywhere(t, svc, &InboundService{}, ib.Id, email, false)
|
|
if tr := trafficOf(t, email); tr.Up != 0 || tr.Down != 0 {
|
|
t.Fatalf("traffic after reset up=%d down=%d, want 0/0", tr.Up, tr.Down)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestTrafficReset_RestoresQuotaDisabledClient(t *testing.T) {
|
|
const email = "depleted@x"
|
|
for _, reset := range trafficResets {
|
|
t.Run(reset.name, func(t *testing.T) {
|
|
setupBulkDB(t)
|
|
ib := seedLocalDisabledClient(t, &ClientService{}, 52021, "", email, 1000, 0, 600, 500)
|
|
|
|
if err := reset.run(ib, email); err != nil {
|
|
t.Fatalf("%s: %v", reset.name, err)
|
|
}
|
|
|
|
if tr := trafficOf(t, email); !tr.Enable || tr.Up != 0 || tr.Down != 0 {
|
|
t.Fatalf("traffic after reset enable=%v up=%d down=%d, want true 0/0", tr.Enable, tr.Up, tr.Down)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// A node cuts a client on the usage its master pushed while the node's own
|
|
// share is still below the quota; a reset must still count that as depleted.
|
|
func TestTrafficReset_RestoresClientDisabledByGlobalUsage(t *testing.T) {
|
|
const email = "shared@x"
|
|
for i, reset := range trafficResets {
|
|
t.Run(reset.name, func(t *testing.T) {
|
|
setupBulkDB(t)
|
|
svc := &ClientService{}
|
|
inboundSvc := &InboundService{}
|
|
ib := seedLocalDisabledClient(t, svc, 52022, "", email, 1000, 0, 100, 100)
|
|
if err := inboundSvc.AcceptGlobalTraffic("master-a", []*xray.ClientTraffic{{Email: email, Up: 800, Down: 700}}); err != nil {
|
|
t.Fatalf("AcceptGlobalTraffic: %v", err)
|
|
}
|
|
|
|
if err := reset.run(ib, email); err != nil {
|
|
t.Fatalf("%s: %v", reset.name, err)
|
|
}
|
|
|
|
if !trafficOf(t, email).Enable {
|
|
t.Fatal("client_traffics.enable = false: the pushed usage was not counted as depleted")
|
|
}
|
|
// Only the ClientService per-client resets restore the record and settings.
|
|
if i < 2 {
|
|
assertEnableEverywhere(t, svc, inboundSvc, ib.Id, email, true)
|
|
}
|
|
})
|
|
}
|
|
}
|