| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211 |
- package service
- import (
- "encoding/json"
- "path/filepath"
- "testing"
- "github.com/mhsanaei/3x-ui/v3/internal/database"
- "github.com/mhsanaei/3x-ui/v3/internal/database/model"
- "gorm.io/gorm"
- )
- const visionTest = "xtls-rprx-vision"
- func clientFlowsInSettings(t *testing.T, settings string) map[string]string {
- t.Helper()
- var parsed map[string]any
- if err := json.Unmarshal([]byte(settings), &parsed); err != nil {
- t.Fatalf("parse settings: %v", err)
- }
- out := map[string]string{}
- clients, _ := parsed["clients"].([]any)
- for _, c := range clients {
- cm, ok := c.(map[string]any)
- if !ok {
- continue
- }
- email, _ := cm["email"].(string)
- flow, _ := cm["flow"].(string)
- out[email] = flow
- }
- return out
- }
- func TestStripClientFlows(t *testing.T) {
- cases := []struct {
- name string
- in string
- wantChanged bool
- wantFlows map[string]string
- }{
- {
- name: "clears vision on all clients",
- in: `{"clients":[{"email":"a","flow":"` + visionTest + `"},{"email":"b","flow":"` + visionTest + `"}]}`,
- wantChanged: true,
- wantFlows: map[string]string{"a": "", "b": ""},
- },
- {
- name: "mixed flows: clears only the non-empty",
- in: `{"clients":[{"email":"a","flow":"` + visionTest + `"},{"email":"b","flow":""}]}`,
- wantChanged: true,
- wantFlows: map[string]string{"a": "", "b": ""},
- },
- {
- name: "no flows: unchanged",
- in: `{"clients":[{"email":"a","flow":""},{"email":"b"}]}`,
- wantChanged: false,
- wantFlows: map[string]string{"a": "", "b": ""},
- },
- {
- name: "no clients: unchanged",
- in: `{"decryption":"none"}`,
- wantChanged: false,
- },
- {
- name: "malformed json: unchanged",
- in: `{not json`,
- wantChanged: false,
- },
- }
- for _, tc := range cases {
- t.Run(tc.name, func(t *testing.T) {
- out, changed := stripClientFlows(tc.in)
- if changed != tc.wantChanged {
- t.Fatalf("changed = %v, want %v", changed, tc.wantChanged)
- }
- if !changed {
- if out != tc.in {
- t.Fatalf("unchanged input must be returned verbatim, got %q", out)
- }
- return
- }
- got := clientFlowsInSettings(t, out)
- for email, want := range tc.wantFlows {
- if got[email] != want {
- t.Errorf("flow[%s] = %q, want %q", email, got[email], want)
- }
- }
- })
- }
- }
- func initFlowTestDB(t *testing.T) *gorm.DB {
- t.Helper()
- dbDir := t.TempDir()
- t.Setenv("XUI_DB_FOLDER", dbDir)
- if err := database.InitDB(filepath.Join(dbDir, "x-ui.db")); err != nil {
- t.Fatalf("InitDB: %v", err)
- }
- t.Cleanup(func() { _ = database.CloseDB() })
- return database.GetDB()
- }
- func TestAddInbound_DisableFlowClampsClientFlow(t *testing.T) {
- initFlowTestDB(t)
- ibSvc := &InboundService{}
- in := &model.Inbound{
- Tag: "dis-add", Enable: true, Port: 52001, Protocol: model.VLESS,
- StreamSettings: `{"network":"tcp","security":"reality"}`,
- Settings: `{"clients":[{"id":"u1","email":"a@x","flow":"` + visionTest + `","subId":"s1","enable":true}]}`,
- DisableFlow: true,
- }
- if _, _, err := ibSvc.AddInbound(in); err != nil {
- t.Fatalf("AddInbound: %v", err)
- }
- got, err := ibSvc.GetInbound(in.Id)
- if err != nil {
- t.Fatalf("GetInbound: %v", err)
- }
- if !got.DisableFlow {
- t.Error("DisableFlow not persisted on created inbound")
- }
- if f := clientFlowsInSettings(t, got.Settings)["a@x"]; f != "" {
- t.Errorf("settings flow = %q, want empty (clamped at creation)", f)
- }
- list, err := ibSvc.clientService.ListForInbound(nil, in.Id)
- if err != nil {
- t.Fatalf("ListForInbound: %v", err)
- }
- if len(list) != 1 || list[0].Flow != "" {
- t.Errorf("flow_override = %#v, want empty (xray must not expect Vision)", list)
- }
- }
- func TestUpdateInbound_DisableFlowPersistsStripsAndResistsRestore(t *testing.T) {
- db := initFlowTestDB(t)
- ibSvc := &InboundService{}
- cs := &ClientService{}
- const email = "shared@x"
- const uid = "ce8d33df-3a64-4f10-8f9b-91c3a8e0d001"
- sibling := &model.Inbound{
- Tag: "sib", Enable: true, Port: 52101, Protocol: model.VLESS,
- StreamSettings: `{"network":"tcp","security":"reality"}`,
- Settings: `{"clients":[{"id":"` + uid + `","email":"` + email + `","flow":"` + visionTest + `","subId":"s1","enable":true}]}`,
- }
- if err := db.Create(sibling).Error; err != nil {
- t.Fatalf("create sibling: %v", err)
- }
- sc, _ := ibSvc.GetClients(sibling)
- if err := cs.SyncInbound(nil, sibling.Id, sc); err != nil {
- t.Fatalf("sync sibling: %v", err)
- }
- target := &model.Inbound{
- Tag: "tgt", Enable: true, Port: 52102, Protocol: model.VLESS,
- StreamSettings: `{"network":"tcp","security":"reality"}`,
- Settings: `{"clients":[{"id":"` + uid + `","email":"` + email + `","flow":"` + visionTest + `","subId":"s1","enable":true}]}`,
- }
- if err := db.Create(target).Error; err != nil {
- t.Fatalf("create target: %v", err)
- }
- tc, _ := ibSvc.GetClients(target)
- if err := cs.SyncInbound(nil, target.Id, tc); err != nil {
- t.Fatalf("sync target: %v", err)
- }
- upd := *target
- upd.DisableFlow = true
- if _, _, err := ibSvc.UpdateInbound(&upd); err != nil {
- t.Fatalf("UpdateInbound: %v", err)
- }
- reloaded, err := ibSvc.GetInbound(target.Id)
- if err != nil {
- t.Fatalf("GetInbound: %v", err)
- }
- if !reloaded.DisableFlow {
- t.Fatal("DisableFlow did not persist through UpdateInbound (blocking regression)")
- }
- if f := clientFlowsInSettings(t, reloaded.Settings)["shared@x"]; f != "" {
- t.Errorf("target settings flow = %q, want empty after disable", f)
- }
- list, err := cs.ListForInbound(nil, target.Id)
- if err != nil {
- t.Fatalf("ListForInbound(target): %v", err)
- }
- if len(list) != 1 || list[0].Flow != "" {
- t.Errorf("target flow_override = %#v, want empty", list)
- }
- ibSvc.MigrationRestoreVisionFlow()
- reloaded2, err := ibSvc.GetInbound(target.Id)
- if err != nil {
- t.Fatalf("GetInbound after restore: %v", err)
- }
- if f := clientFlowsInSettings(t, reloaded2.Settings)["shared@x"]; f != "" {
- t.Errorf("after MigrationRestoreVisionFlow target flow = %q, want empty (must not self-revert)", f)
- }
- sList, err := cs.ListForInbound(nil, sibling.Id)
- if err != nil {
- t.Fatalf("ListForInbound(sibling): %v", err)
- }
- if len(sList) != 1 || sList[0].Flow != visionTest {
- t.Errorf("sibling flow_override = %#v, want Vision preserved", sList)
- }
- }
|