|
@@ -0,0 +1,103 @@
|
|
|
|
|
+package service
|
|
|
|
|
+
|
|
|
|
|
+import (
|
|
|
|
|
+ "testing"
|
|
|
|
|
+
|
|
|
|
|
+ "github.com/mhsanaei/3x-ui/v3/internal/database"
|
|
|
|
|
+ "github.com/mhsanaei/3x-ui/v3/internal/database/model"
|
|
|
|
|
+)
|
|
|
|
|
+
|
|
|
|
|
+// TestAddInbound_ImportConvertsExternalProxyToHosts reproduces the panel report:
|
|
|
|
|
+// an inbound exported from a build that predated the hosts table carries its
|
|
|
|
|
+// external proxies inline in streamSettings.externalProxy. The one-time startup
|
|
|
|
|
+// migration that converts those to host rows is gated off after first run, so a
|
|
|
|
|
+// freshly imported inbound used to land with zero hosts (its external proxies
|
|
|
|
|
+// silently lost). AddInbound must convert them on import.
|
|
|
|
|
+func TestAddInbound_ImportConvertsExternalProxyToHosts(t *testing.T) {
|
|
|
|
|
+ setupConflictDB(t)
|
|
|
|
|
+ svc := &InboundService{}
|
|
|
|
|
+
|
|
|
|
|
+ stream := `{
|
|
|
|
|
+ "network":"ws",
|
|
|
|
|
+ "wsSettings":{"path":"/req3","host":"astr.khafanha.ir"},
|
|
|
|
|
+ "security":"none",
|
|
|
|
|
+ "externalProxy":[
|
|
|
|
|
+ {"forceTls":"same","dest":"snapp.ir","port":8080,"remark":"","sni":"","alpn":[],"pinnedPeerCertSha256":[],"echConfigList":""},
|
|
|
|
|
+ {"forceTls":"tls","dest":"cdn.example.com","port":8443,"remark":"front","sni":"sni.example.com","fingerprint":"chrome","alpn":["h2","h3"],"pinnedPeerCertSha256":["AAAA"],"echConfigList":"ECHV"}
|
|
|
|
|
+ ]
|
|
|
|
|
+ }`
|
|
|
|
|
+ settings := `{"clients":[{"id":"6df5616b-ebfd-4186-86d5-4bce29fe8805","email":"imp_user","subId":"s-imp","enable":true}],"decryption":"none","encryption":"none"}`
|
|
|
|
|
+
|
|
|
|
|
+ in := &model.Inbound{
|
|
|
|
|
+ UserId: 1,
|
|
|
|
|
+ Tag: "in-8080-tcp",
|
|
|
|
|
+ Enable: true,
|
|
|
|
|
+ Listen: "",
|
|
|
|
|
+ Port: 8080,
|
|
|
|
|
+ Protocol: model.VLESS,
|
|
|
|
|
+ StreamSettings: stream,
|
|
|
|
|
+ Settings: settings,
|
|
|
|
|
+ }
|
|
|
|
|
+ created, _, err := svc.AddInbound(in)
|
|
|
|
|
+ if err != nil {
|
|
|
|
|
+ t.Fatalf("import inbound: %v", err)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ var hosts []model.Host
|
|
|
|
|
+ if err := database.GetDB().Where("inbound_id = ?", created.Id).Order("sort_order asc").Find(&hosts).Error; err != nil {
|
|
|
|
|
+ t.Fatalf("load hosts: %v", err)
|
|
|
|
|
+ }
|
|
|
|
|
+ if len(hosts) != 2 {
|
|
|
|
|
+ t.Fatalf("hosts = %d, want 2 (one per externalProxy entry)", len(hosts))
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ a := hosts[0]
|
|
|
|
|
+ if a.SortOrder != 0 || a.Security != "same" || a.Address != "snapp.ir" || a.Port != 8080 {
|
|
|
|
|
+ t.Fatalf("host A mapping wrong: %+v", a)
|
|
|
|
|
+ }
|
|
|
|
|
+ if a.Remark == "" {
|
|
|
|
|
+ t.Fatalf("host A remark must be backfilled for a blank externalProxy remark, got empty")
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ b := hosts[1]
|
|
|
|
|
+ if b.SortOrder != 1 || b.Security != "tls" || b.Address != "cdn.example.com" || b.Port != 8443 ||
|
|
|
|
|
+ b.Remark != "front" || b.Sni != "sni.example.com" || b.Fingerprint != "chrome" || b.EchConfigList != "ECHV" {
|
|
|
|
|
+ t.Fatalf("host B mapping wrong: %+v", b)
|
|
|
|
|
+ }
|
|
|
|
|
+ if len(b.Alpn) != 2 || b.Alpn[0] != "h2" || b.Alpn[1] != "h3" {
|
|
|
|
|
+ t.Fatalf("host B alpn = %v, want [h2 h3]", b.Alpn)
|
|
|
|
|
+ }
|
|
|
|
|
+ if len(b.PinnedPeerCertSha256) != 1 || b.PinnedPeerCertSha256[0] != "AAAA" {
|
|
|
|
|
+ t.Fatalf("host B pins = %v, want [AAAA]", b.PinnedPeerCertSha256)
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// TestAddInbound_NoExternalProxyCreatesNoHosts guards the no-op path: an inbound
|
|
|
|
|
+// built by the current UI (no externalProxy) must not gain phantom host rows.
|
|
|
|
|
+func TestAddInbound_NoExternalProxyCreatesNoHosts(t *testing.T) {
|
|
|
|
|
+ setupConflictDB(t)
|
|
|
|
|
+ svc := &InboundService{}
|
|
|
|
|
+
|
|
|
|
|
+ in := &model.Inbound{
|
|
|
|
|
+ UserId: 1,
|
|
|
|
|
+ Tag: "in-9201-tcp",
|
|
|
|
|
+ Enable: true,
|
|
|
|
|
+ Listen: "0.0.0.0",
|
|
|
|
|
+ Port: 9201,
|
|
|
|
|
+ Protocol: model.VLESS,
|
|
|
|
|
+ StreamSettings: `{"network":"tcp","security":"none"}`,
|
|
|
|
|
+ Settings: `{"clients":[{"id":"77777777-7777-7777-7777-777777777777","email":"plain","subId":"s-plain","enable":true}],"decryption":"none","encryption":"none"}`,
|
|
|
|
|
+ }
|
|
|
|
|
+ created, _, err := svc.AddInbound(in)
|
|
|
|
|
+ if err != nil {
|
|
|
|
|
+ t.Fatalf("add inbound: %v", err)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ var count int64
|
|
|
|
|
+ if err := database.GetDB().Model(&model.Host{}).Where("inbound_id = ?", created.Id).Count(&count).Error; err != nil {
|
|
|
|
|
+ t.Fatalf("count hosts: %v", err)
|
|
|
|
|
+ }
|
|
|
|
|
+ if count != 0 {
|
|
|
|
|
+ t.Fatalf("host count = %d, want 0", count)
|
|
|
|
|
+ }
|
|
|
|
|
+}
|