| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137 |
- package service
- import (
- "slices"
- "strings"
- "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/mtproto"
- "github.com/mhsanaei/3x-ui/v3/internal/web/runtime"
- "github.com/mhsanaei/3x-ui/v3/internal/xray"
- )
- // startQuotaSidecar runs a local MTProto inbound for mtga and mtgb under the fake
- // mtg and returns its API log once the sidecar answers.
- func startQuotaSidecar(t *testing.T, port int, mtga model.Client) (*model.Inbound, string) {
- t.Helper()
- setupConflictDB(t)
- pidFile, logPath := installFakeMtgAPI(t)
- runtime.SetManager(runtime.NewManager(runtime.LocalDeps{APIPort: func() int { return 0 }, SetNeedRestart: func() {}}))
- t.Cleanup(func() { runtime.SetManager(nil) })
- mtga.Email, mtga.Secret = "mtga", mtprotoTestSecretA
- clients := []model.Client{mtga, {Email: "mtgb", Secret: mtprotoTestSecretB, Enable: true}}
- ib := &model.Inbound{Tag: "mt-quota", Enable: true, Port: port, Protocol: model.MTProto, Settings: clientsSettings(t, clients)}
- if err := database.GetDB().Create(ib).Error; err != nil {
- t.Fatalf("create inbound: %v", err)
- }
- if err := (&ClientService{}).SyncInbound(nil, ib.Id, clients); err != nil {
- t.Fatalf("SyncInbound: %v", err)
- }
- for _, c := range clients {
- row := xray.ClientTraffic{InboundId: ib.Id, Email: c.Email, Enable: true, Up: 5, Total: c.TotalGB, ExpiryTime: c.ExpiryTime, Reset: c.Reset}
- if err := database.GetDB().Create(&row).Error; err != nil {
- t.Fatalf("seed traffic: %v", err)
- }
- }
- // A running sidecar needs a served client, so prime with the healthy set.
- inst, ok := mtproto.InstanceFromInbound(&model.Inbound{
- Id: ib.Id, Tag: ib.Tag, Port: port, Protocol: model.MTProto,
- Settings: clientsSettings(t, []model.Client{{Email: "mtgb", Secret: mtprotoTestSecretB, Enable: true}}),
- })
- if !ok {
- t.Fatal("seed inbound must produce an mtg instance")
- }
- if err := mtproto.GetManager().Ensure(inst); err != nil {
- t.Fatalf("start mtg: %v", err)
- }
- t.Cleanup(func() { mtproto.GetManager().Remove(ib.Id) })
- waitForSpawns(t, pidFile, 1)
- waitFakeMtgLog(t, logPath, "ready")
- return ib, logPath
- }
- func quotaResets(t *testing.T, logPath string) []string {
- t.Helper()
- var out []string
- for _, line := range fakeMtgLog(t, logPath) {
- if name, ok := strings.CutPrefix(line, "reset:"); ok {
- out = append(out, name)
- }
- }
- slices.Sort(out)
- return out
- }
- // Every path that zeroes a client's panel counters must zero the sidecar's own
- // quota counter too, or the sidecar keeps refusing the client.
- func TestPanelResetsZeroSidecarQuota(t *testing.T) {
- t.Run("bulk reset", func(t *testing.T) {
- _, logPath := startQuotaSidecar(t, 46201, model.Client{Enable: true})
- if _, err := (&ClientService{}).BulkResetTraffic(&InboundService{}, []string{"mtga"}); err != nil {
- t.Fatalf("BulkResetTraffic: %v", err)
- }
- if got := quotaResets(t, logPath); !slices.Equal(got, []string{"mtga"}) {
- t.Fatalf("sidecar quota resets %v, want [mtga]", got)
- }
- })
- t.Run("inbound clients", func(t *testing.T) {
- ib, logPath := startQuotaSidecar(t, 46207, model.Client{Enable: true})
- if err := (&ClientService{}).ResetAllClientTraffics(&InboundService{}, ib.Id); err != nil {
- t.Fatalf("ResetAllClientTraffics: %v", err)
- }
- if got := quotaResets(t, logPath); !slices.Equal(got, []string{"mtga", "mtgb"}) {
- t.Fatalf("sidecar quota resets %v, want [mtga mtgb]", got)
- }
- })
- t.Run("reset all", func(t *testing.T) {
- _, logPath := startQuotaSidecar(t, 46202, model.Client{Enable: true})
- if _, err := (&ClientService{}).ResetAllTraffics(); err != nil {
- t.Fatalf("ResetAllTraffics: %v", err)
- }
- if got := quotaResets(t, logPath); !slices.Equal(got, []string{"mtga", "mtgb"}) {
- t.Fatalf("sidecar quota resets %v, want [mtga mtgb]", got)
- }
- })
- t.Run("auto renew", func(t *testing.T) {
- expired := time.Now().Add(-time.Hour).UnixMilli()
- _, logPath := startQuotaSidecar(t, 46203, model.Client{Enable: true, Reset: 30, ExpiryTime: expired})
- if _, _, err := (&InboundService{}).AddTraffic(nil, nil); err != nil {
- t.Fatalf("AddTraffic: %v", err)
- }
- if got := quotaResets(t, logPath); !slices.Equal(got, []string{"mtga"}) {
- t.Fatalf("sidecar quota resets %v, want [mtga]", got)
- }
- })
- }
- // Resetting inbound counters leaves every client's usage in place, so the
- // sidecar's quota counters must stay too or clients get their quota again free.
- func TestInboundResetAllKeepsSidecarQuota(t *testing.T) {
- _, logPath := startQuotaSidecar(t, 46204, model.Client{Enable: true})
- if err := (&InboundService{}).ResetAllTraffics(); err != nil {
- t.Fatalf("ResetAllTraffics: %v", err)
- }
- if got := quotaResets(t, logPath); len(got) != 0 {
- t.Fatalf("inbound reset zeroed sidecar quotas %v, want none", got)
- }
- }
- // Resetting one inbound's clients zeroes only their sidecar quotas, not those
- // of MTProto clients whose usage the reset left in place.
- func TestInboundClientResetKeepsOtherSidecarQuotas(t *testing.T) {
- _, logPath := startQuotaSidecar(t, 46205, model.Client{Enable: true})
- other := mkInbound(t, 46206, model.VLESS, clientsSettings(t, []model.Client{{Email: "vless-only", ID: "11111111-1111-1111-1111-1111111111ab", Enable: true}}))
- if err := (&ClientService{}).SyncInbound(nil, other.Id, []model.Client{{Email: "vless-only", ID: "11111111-1111-1111-1111-1111111111ab", Enable: true}}); err != nil {
- t.Fatalf("SyncInbound: %v", err)
- }
- if err := (&ClientService{}).ResetAllClientTraffics(&InboundService{}, other.Id); err != nil {
- t.Fatalf("ResetAllClientTraffics: %v", err)
- }
- if got := quotaResets(t, logPath); len(got) != 0 {
- t.Fatalf("resetting another inbound zeroed sidecar quotas %v, want none", got)
- }
- }
|