|
|
@@ -5,25 +5,28 @@ import (
|
|
|
|
|
|
"github.com/mhsanaei/3x-ui/v3/internal/database"
|
|
|
"github.com/mhsanaei/3x-ui/v3/internal/database/model"
|
|
|
+ "github.com/mhsanaei/3x-ui/v3/internal/web/entity"
|
|
|
)
|
|
|
|
|
|
-func mkHost(t *testing.T, svc *HostService, inboundId int, remark string, order int) *model.Host {
|
|
|
+func mkHost(t *testing.T, svc *HostService, inboundId int, remark string, order int) *entity.HostGroup {
|
|
|
t.Helper()
|
|
|
- h, err := svc.AddHost(&model.Host{
|
|
|
- InboundId: inboundId,
|
|
|
- Remark: remark,
|
|
|
- SortOrder: order,
|
|
|
- Address: remark + ".example.com",
|
|
|
- Port: 8443,
|
|
|
+ created, err := svc.AddHostGroup(&entity.HostGroup{
|
|
|
+ InboundIds: []int{inboundId},
|
|
|
+ Remark: remark,
|
|
|
+ SortOrder: order,
|
|
|
+ Hosts: []string{remark + ".example.com"},
|
|
|
+ Port: 8443,
|
|
|
})
|
|
|
if err != nil {
|
|
|
- t.Fatalf("AddHost %s: %v", remark, err)
|
|
|
+ t.Fatalf("AddHostGroup %s: %v", remark, err)
|
|
|
}
|
|
|
- return h
|
|
|
+ g, err := svc.GetHostGroup(created[0].GroupId)
|
|
|
+ if err != nil {
|
|
|
+ t.Fatalf("GetHostGroup %s: %v", remark, err)
|
|
|
+ }
|
|
|
+ return g
|
|
|
}
|
|
|
|
|
|
-// TestAddHost_GetHostsByInbound: create persists; query returns by inbound,
|
|
|
-// ordered by sort_order then id.
|
|
|
func TestAddHost_GetHostsByInbound(t *testing.T) {
|
|
|
setupBulkDB(t)
|
|
|
svc := &HostService{}
|
|
|
@@ -38,24 +41,22 @@ func TestAddHost_GetHostsByInbound(t *testing.T) {
|
|
|
if len(got) != 2 {
|
|
|
t.Fatalf("len = %d, want 2", len(got))
|
|
|
}
|
|
|
- if got[0].Id != h2.Id || got[1].Id != h1.Id {
|
|
|
- t.Fatalf("order = [%d,%d], want [%d,%d] (sort_order asc)", got[0].Id, got[1].Id, h2.Id, h1.Id)
|
|
|
+ if got[0].GroupId != h2.GroupId || got[1].GroupId != h1.GroupId {
|
|
|
+ t.Fatalf("order = [%s,%s], want [%s,%s] (sort_order asc)", got[0].GroupId, got[1].GroupId, h2.GroupId, h1.GroupId)
|
|
|
}
|
|
|
- if got[0].Address != "a.example.com" {
|
|
|
- t.Fatalf("address not persisted: %q", got[0].Address)
|
|
|
+ if got[0].Hosts[0] != "a.example.com:8443" {
|
|
|
+ t.Fatalf("address not persisted: %q", got[0].Hosts[0])
|
|
|
}
|
|
|
}
|
|
|
|
|
|
-// TestAddHost_RejectsUnknownInbound: a host whose inbound does not exist is refused.
|
|
|
func TestAddHost_RejectsUnknownInbound(t *testing.T) {
|
|
|
setupBulkDB(t)
|
|
|
svc := &HostService{}
|
|
|
- if _, err := svc.AddHost(&model.Host{InboundId: 99999, Remark: "x"}); err == nil {
|
|
|
+ if _, err := svc.AddHostGroup(&entity.HostGroup{InboundIds: []int{99999}, Remark: "x", Hosts: []string{"test.com"}}); err == nil {
|
|
|
t.Fatalf("expected error adding host to unknown inbound")
|
|
|
}
|
|
|
}
|
|
|
|
|
|
-// TestReorderHosts: reorder updates sort_order and re-query reflects new order.
|
|
|
func TestReorderHosts(t *testing.T) {
|
|
|
setupBulkDB(t)
|
|
|
svc := &HostService{}
|
|
|
@@ -64,22 +65,21 @@ func TestReorderHosts(t *testing.T) {
|
|
|
h2 := mkHost(t, svc, ib.Id, "h2", 0)
|
|
|
h3 := mkHost(t, svc, ib.Id, "h3", 0)
|
|
|
|
|
|
- want := []int{h3.Id, h1.Id, h2.Id}
|
|
|
- if err := svc.ReorderHosts(want); err != nil {
|
|
|
- t.Fatalf("ReorderHosts: %v", err)
|
|
|
+ want := []string{h3.GroupId, h1.GroupId, h2.GroupId}
|
|
|
+ if err := svc.ReorderHostGroups(want); err != nil {
|
|
|
+ t.Fatalf("ReorderHostGroups: %v", err)
|
|
|
}
|
|
|
got, _ := svc.GetHostsByInbound(ib.Id)
|
|
|
- for i, h := range got {
|
|
|
- if h.Id != want[i] {
|
|
|
- t.Fatalf("position %d = %d, want %d", i, h.Id, want[i])
|
|
|
+ for i, g := range got {
|
|
|
+ if g.GroupId != want[i] {
|
|
|
+ t.Fatalf("position %d = %s, want %s", i, g.GroupId, want[i])
|
|
|
}
|
|
|
- if h.SortOrder != i {
|
|
|
- t.Fatalf("host %d sort_order = %d, want %d", h.Id, h.SortOrder, i)
|
|
|
+ if g.SortOrder != i {
|
|
|
+ t.Fatalf("host %s sort_order = %d, want %d", g.GroupId, g.SortOrder, i)
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
-// TestSetHostEnableAndBulk: per-row and bulk enable/disable toggles persist.
|
|
|
func TestSetHostEnableAndBulk(t *testing.T) {
|
|
|
setupBulkDB(t)
|
|
|
svc := &HostService{}
|
|
|
@@ -87,32 +87,31 @@ func TestSetHostEnableAndBulk(t *testing.T) {
|
|
|
h1 := mkHost(t, svc, ib.Id, "h1", 0)
|
|
|
h2 := mkHost(t, svc, ib.Id, "h2", 1)
|
|
|
|
|
|
- if err := svc.SetHostEnable(h1.Id, false); err != nil {
|
|
|
- t.Fatalf("SetHostEnable: %v", err)
|
|
|
+ if err := svc.SetHostGroupEnable(h1.GroupId, false); err != nil {
|
|
|
+ t.Fatalf("SetHostGroupEnable: %v", err)
|
|
|
}
|
|
|
- if g, _ := svc.GetHost(h1.Id); g == nil || !g.IsDisabled {
|
|
|
- t.Fatalf("h1 should be disabled after SetHostEnable(false)")
|
|
|
+ if g, _ := svc.GetHostGroup(h1.GroupId); g == nil || !g.IsDisabled {
|
|
|
+ t.Fatalf("h1 should be disabled after SetHostGroupEnable(false)")
|
|
|
}
|
|
|
|
|
|
- if err := svc.SetHostsEnable([]int{h1.Id, h2.Id}, true); err != nil {
|
|
|
- t.Fatalf("SetHostsEnable(true): %v", err)
|
|
|
+ if err := svc.SetHostsGroupEnable([]string{h1.GroupId, h2.GroupId}, true); err != nil {
|
|
|
+ t.Fatalf("SetHostsGroupEnable(true): %v", err)
|
|
|
}
|
|
|
- for _, id := range []int{h1.Id, h2.Id} {
|
|
|
- if g, _ := svc.GetHost(id); g == nil || g.IsDisabled {
|
|
|
- t.Fatalf("host %d should be enabled", id)
|
|
|
+ for _, gid := range []string{h1.GroupId, h2.GroupId} {
|
|
|
+ if g, _ := svc.GetHostGroup(gid); g == nil || g.IsDisabled {
|
|
|
+ t.Fatalf("host %s should be enabled", gid)
|
|
|
}
|
|
|
}
|
|
|
- if err := svc.SetHostsEnable([]int{h1.Id, h2.Id}, false); err != nil {
|
|
|
- t.Fatalf("SetHostsEnable(false): %v", err)
|
|
|
+ if err := svc.SetHostsGroupEnable([]string{h1.GroupId, h2.GroupId}, false); err != nil {
|
|
|
+ t.Fatalf("SetHostsGroupEnable(false): %v", err)
|
|
|
}
|
|
|
- for _, id := range []int{h1.Id, h2.Id} {
|
|
|
- if g, _ := svc.GetHost(id); g == nil || !g.IsDisabled {
|
|
|
- t.Fatalf("host %d should be disabled", id)
|
|
|
+ for _, gid := range []string{h1.GroupId, h2.GroupId} {
|
|
|
+ if g, _ := svc.GetHostGroup(gid); g == nil || !g.IsDisabled {
|
|
|
+ t.Fatalf("host %s should be disabled", gid)
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
-// TestDeleteHosts: bulk delete removes exactly the named rows.
|
|
|
func TestDeleteHosts(t *testing.T) {
|
|
|
setupBulkDB(t)
|
|
|
svc := &HostService{}
|
|
|
@@ -121,27 +120,24 @@ func TestDeleteHosts(t *testing.T) {
|
|
|
h2 := mkHost(t, svc, ib.Id, "h2", 1)
|
|
|
h3 := mkHost(t, svc, ib.Id, "h3", 2)
|
|
|
|
|
|
- if err := svc.DeleteHosts([]int{h1.Id, h3.Id}); err != nil {
|
|
|
- t.Fatalf("DeleteHosts: %v", err)
|
|
|
+ if err := svc.DeleteHostsGroup([]string{h1.GroupId, h3.GroupId}); err != nil {
|
|
|
+ t.Fatalf("DeleteHostsGroup: %v", err)
|
|
|
}
|
|
|
got, _ := svc.GetHostsByInbound(ib.Id)
|
|
|
- if len(got) != 1 || got[0].Id != h2.Id {
|
|
|
- t.Fatalf("remaining = %v, want only h2 (%d)", got, h2.Id)
|
|
|
+ if len(got) != 1 || got[0].GroupId != h2.GroupId {
|
|
|
+ t.Fatalf("remaining = %v, want only h2 (%s)", got, h2.GroupId)
|
|
|
}
|
|
|
}
|
|
|
|
|
|
-// TestDeleteInboundCascadesHosts: deleting an inbound deletes its hosts.
|
|
|
func TestDeleteInboundCascadesHosts(t *testing.T) {
|
|
|
setupBulkDB(t)
|
|
|
svc := &HostService{}
|
|
|
inboundSvc := &InboundService{}
|
|
|
- // Disabled local inbound so DelInbound skips the runtime push.
|
|
|
ib := &model.Inbound{Tag: "casc", Enable: false, Port: 4443, Protocol: model.VLESS, Settings: `{"clients":[]}`}
|
|
|
if err := database.GetDB().Create(ib).Error; err != nil {
|
|
|
t.Fatalf("create inbound: %v", err)
|
|
|
}
|
|
|
- mkHost(t, svc, ib.Id, "h1", 0)
|
|
|
- mkHost(t, svc, ib.Id, "h2", 1)
|
|
|
+ h1 := mkHost(t, svc, ib.Id, "h1", 0)
|
|
|
|
|
|
if _, err := inboundSvc.DelInbound(ib.Id); err != nil {
|
|
|
t.Fatalf("DelInbound: %v", err)
|
|
|
@@ -150,18 +146,20 @@ func TestDeleteInboundCascadesHosts(t *testing.T) {
|
|
|
if len(got) != 0 {
|
|
|
t.Fatalf("hosts not cascaded on inbound delete, len = %d", len(got))
|
|
|
}
|
|
|
+ if _, err := svc.GetHostGroup(h1.GroupId); err == nil {
|
|
|
+ t.Fatalf("expected group to be deleted after cascading")
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
-// TestGetAllTags: distinct, sorted tags across all hosts.
|
|
|
func TestGetAllTags(t *testing.T) {
|
|
|
setupBulkDB(t)
|
|
|
svc := &HostService{}
|
|
|
ib := mkInbound(t, 443, model.VLESS, `{"clients":[]}`)
|
|
|
- if _, err := svc.AddHost(&model.Host{InboundId: ib.Id, Remark: "h1", Tags: []string{"EU", "CDN"}}); err != nil {
|
|
|
- t.Fatalf("AddHost: %v", err)
|
|
|
+ if _, err := svc.AddHostGroup(&entity.HostGroup{InboundIds: []int{ib.Id}, Remark: "h1", Hosts: []string{"h1.com"}, Tags: []string{"EU", "CDN"}}); err != nil {
|
|
|
+ t.Fatalf("AddHostGroup: %v", err)
|
|
|
}
|
|
|
- if _, err := svc.AddHost(&model.Host{InboundId: ib.Id, Remark: "h2", Tags: []string{"CDN", "FAST"}}); err != nil {
|
|
|
- t.Fatalf("AddHost: %v", err)
|
|
|
+ if _, err := svc.AddHostGroup(&entity.HostGroup{InboundIds: []int{ib.Id}, Remark: "h2", Hosts: []string{"h2.com"}, Tags: []string{"CDN", "FAST"}}); err != nil {
|
|
|
+ t.Fatalf("AddHostGroup: %v", err)
|
|
|
}
|
|
|
tags, err := svc.GetAllTags()
|
|
|
if err != nil {
|
|
|
@@ -177,3 +175,193 @@ func TestGetAllTags(t *testing.T) {
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
+
|
|
|
+func TestAddHostsGroup(t *testing.T) {
|
|
|
+ setupBulkDB(t)
|
|
|
+ svc := &HostService{}
|
|
|
+ ib1 := mkInbound(t, 443, model.VLESS, `{"clients":[]}`)
|
|
|
+ ib2 := mkInbound(t, 80, model.VLESS, `{"clients":[]}`)
|
|
|
+
|
|
|
+ req := &entity.HostGroup{
|
|
|
+ InboundIds: []int{ib1.Id, ib2.Id},
|
|
|
+ Hosts: []string{"h1.com", "h2.com:443", "[2001:db8::1]:80"},
|
|
|
+ Remark: "BulkRemark",
|
|
|
+ Port: 8443,
|
|
|
+ Security: "same",
|
|
|
+ }
|
|
|
+
|
|
|
+ created, err := svc.AddHostGroup(req)
|
|
|
+ if err != nil {
|
|
|
+ t.Fatalf("AddHostGroup: %v", err)
|
|
|
+ }
|
|
|
+
|
|
|
+ if len(created) != 6 {
|
|
|
+ t.Fatalf("expected 6 created hosts, got %d", len(created))
|
|
|
+ }
|
|
|
+
|
|
|
+ got1, _ := svc.GetHostsByInbound(ib1.Id)
|
|
|
+ if len(got1) != 1 {
|
|
|
+ t.Fatalf("expected 1 group for inbound 1, got %d", len(got1))
|
|
|
+ }
|
|
|
+
|
|
|
+ g := got1[0]
|
|
|
+ if g.Remark != "BulkRemark" {
|
|
|
+ t.Errorf("expected remark BulkRemark, got %s", g.Remark)
|
|
|
+ }
|
|
|
+
|
|
|
+ var foundH2Port443 bool
|
|
|
+ var foundIPv6Port80 bool
|
|
|
+ var foundH1DefaultPort8443 bool
|
|
|
+
|
|
|
+ for _, hostStr := range g.Hosts {
|
|
|
+ if hostStr == "h2.com:443" {
|
|
|
+ foundH2Port443 = true
|
|
|
+ }
|
|
|
+ if hostStr == "[2001:db8::1]:80" {
|
|
|
+ foundIPv6Port80 = true
|
|
|
+ }
|
|
|
+ if hostStr == "h1.com:8443" {
|
|
|
+ foundH1DefaultPort8443 = true
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if !foundH2Port443 {
|
|
|
+ t.Error("missing custom port override host h2.com:443")
|
|
|
+ }
|
|
|
+ if !foundIPv6Port80 {
|
|
|
+ t.Error("missing IPv6 host with port override [2001:db8::1]:80")
|
|
|
+ }
|
|
|
+ if !foundH1DefaultPort8443 {
|
|
|
+ t.Error("missing default port fallback host h1.com:8443")
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+func TestParseHostAndPort_IPv6EdgeCases(t *testing.T) {
|
|
|
+ tests := []struct {
|
|
|
+ input string
|
|
|
+ defaultPort int
|
|
|
+ wantAddr string
|
|
|
+ wantPort int
|
|
|
+ }{
|
|
|
+ {"2001:db8::1", 8443, "2001:db8::1", 8443},
|
|
|
+ {"[2001:db8::1]:80", 8443, "2001:db8::1", 80},
|
|
|
+ {"h1.com:443", 8443, "h1.com", 443},
|
|
|
+ {"h1.com", 8443, "h1.com", 8443},
|
|
|
+ }
|
|
|
+
|
|
|
+ for _, tc := range tests {
|
|
|
+ addr, port := parseHostAndPort(tc.input, tc.defaultPort)
|
|
|
+ if addr != tc.wantAddr || port != tc.wantPort {
|
|
|
+ t.Errorf("parseHostAndPort(%q, %d) = (%q, %d); want (%q, %d)",
|
|
|
+ tc.input, tc.defaultPort, addr, port, tc.wantAddr, tc.wantPort)
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+func TestParseHostAndPort_AdversarialStressCases(t *testing.T) {
|
|
|
+ tests := []struct {
|
|
|
+ input string
|
|
|
+ defaultPort int
|
|
|
+ wantAddr string
|
|
|
+ wantPort int
|
|
|
+ }{
|
|
|
+ {"", 8443, "", 8443},
|
|
|
+ {" ", 8443, "", 8443},
|
|
|
+ {"h1.com: ", 8443, "h1.com:", 8443},
|
|
|
+ {"h1.com: -1", 8443, "h1.com: -1", 8443},
|
|
|
+ {"h1.com:-1", 8443, "h1.com:-1", 8443},
|
|
|
+ {"h1.com:0", 8443, "h1.com", 0},
|
|
|
+ {"h1.com:65535", 8443, "h1.com", 65535},
|
|
|
+ {"h1.com:65536", 8443, "h1.com:65536", 8443},
|
|
|
+ {"h1.com:80a", 8443, "h1.com:80a", 8443},
|
|
|
+ {"h1.com:123:456", 8443, "h1.com:123:456", 8443},
|
|
|
+ {"[2001:db8::1]", 8443, "2001:db8::1", 8443},
|
|
|
+ {"[2001:db8::1]:80", 8443, "2001:db8::1", 80},
|
|
|
+ {"2001:db8::1", 8443, "2001:db8::1", 8443},
|
|
|
+ {"[2001:db8::1]:65536", 8443, "[2001:db8::1]:65536", 8443},
|
|
|
+ {"[]:80", 8443, "", 80},
|
|
|
+ {"[:]::80", 8443, "[:]:", 80},
|
|
|
+ {"h1.com:", 8443, "h1.com:", 8443},
|
|
|
+ {"h1.com:123:", 8443, "h1.com:123:", 8443},
|
|
|
+ {" h1.com : 80 ", 8443, "h1.com : 80", 8443},
|
|
|
+ {" [2001:db8::1]:80 ", 8443, "2001:db8::1", 80},
|
|
|
+ {"[2001:db8::1]:+80", 8443, "2001:db8::1", 80},
|
|
|
+ {"[2001:db8::1]:080", 8443, "2001:db8::1", 80},
|
|
|
+ {"[2001:db8::1]80", 8443, "[2001:db8::1]80", 8443},
|
|
|
+ {"[::1]", 8443, "::1", 8443},
|
|
|
+ {"[2001:db8::1", 8443, "[2001:db8:", 1},
|
|
|
+ {"[2001:db8::1]:-80", 8443, "[2001:db8::1]:-80", 8443},
|
|
|
+ {"h1.com:443:80", 8443, "h1.com:443:80", 8443},
|
|
|
+ {"[2001:db8::1]::80", 8443, "[2001:db8::1]:", 80},
|
|
|
+ }
|
|
|
+
|
|
|
+ for _, tc := range tests {
|
|
|
+ addr, port := parseHostAndPort(tc.input, tc.defaultPort)
|
|
|
+ if addr != tc.wantAddr || port != tc.wantPort {
|
|
|
+ t.Errorf("parseHostAndPort(%q, %d) = (%q, %d); want (%q, %d)",
|
|
|
+ tc.input, tc.defaultPort, addr, port, tc.wantAddr, tc.wantPort)
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+func TestAddHostGroup_OptionalAddress(t *testing.T) {
|
|
|
+ setupBulkDB(t)
|
|
|
+ svc := &HostService{}
|
|
|
+ ib := mkInbound(t, 443, model.VLESS, `{"clients":[]}`)
|
|
|
+
|
|
|
+ created, err := svc.AddHostGroup(&entity.HostGroup{
|
|
|
+ InboundIds: []int{ib.Id},
|
|
|
+ Remark: "OptionalAddressHost",
|
|
|
+ Hosts: nil,
|
|
|
+ Port: 8443,
|
|
|
+ })
|
|
|
+ if err != nil {
|
|
|
+ t.Fatalf("AddHostGroup with nil Hosts failed: %v", err)
|
|
|
+ }
|
|
|
+
|
|
|
+ if len(created) != 1 {
|
|
|
+ t.Fatalf("expected 1 host created, got %d", len(created))
|
|
|
+ }
|
|
|
+
|
|
|
+ g, err := svc.GetHostGroup(created[0].GroupId)
|
|
|
+ if err != nil {
|
|
|
+ t.Fatalf("GetHostGroup failed: %v", err)
|
|
|
+ }
|
|
|
+
|
|
|
+ if len(g.Hosts) != 1 || g.Hosts[0] != ":8443" {
|
|
|
+ t.Fatalf("expected Hosts list to contain default port fallback ':8443', got %v", g.Hosts)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+func TestUpdateHostGroup_ValidateBeforeDelete(t *testing.T) {
|
|
|
+ setupBulkDB(t)
|
|
|
+ svc := &HostService{}
|
|
|
+ ib := mkInbound(t, 443, model.VLESS, `{"clients":[]}`)
|
|
|
+ h1 := mkHost(t, svc, ib.Id, "h1", 0)
|
|
|
+
|
|
|
+ req := &entity.HostGroup{
|
|
|
+ InboundIds: []int{99999},
|
|
|
+ Remark: "h1-updated",
|
|
|
+ Hosts: []string{"h1.com"},
|
|
|
+ }
|
|
|
+ if _, err := svc.UpdateHostGroup(h1.GroupId, req); err == nil {
|
|
|
+ t.Fatalf("expected error updating host group with invalid inbound")
|
|
|
+ }
|
|
|
+
|
|
|
+ got, err := svc.GetHostGroup(h1.GroupId)
|
|
|
+ if err != nil {
|
|
|
+ t.Fatalf("original host group should not be deleted: %v", err)
|
|
|
+ }
|
|
|
+ if got.Remark != "h1" {
|
|
|
+ t.Fatalf("original host group remark changed: %s", got.Remark)
|
|
|
+ }
|
|
|
+
|
|
|
+ req.InboundIds = []int{ib.Id}
|
|
|
+ if _, err := svc.UpdateHostGroup(h1.GroupId, req); err != nil {
|
|
|
+ t.Fatalf("valid update failed: %v", err)
|
|
|
+ }
|
|
|
+ got2, _ := svc.GetHostGroup(h1.GroupId)
|
|
|
+ if got2.Remark != "h1-updated" {
|
|
|
+ t.Fatalf("remark not updated: %s", got2.Remark)
|
|
|
+ }
|
|
|
+}
|