Files
3x-ui/internal/web/service/inbound_import_external_proxy_test.go
T
MHSanaei 39eb5baf42 fix(inbound): convert legacy externalProxy to hosts on import
An inbound exported from a build that predated the hosts table carries
its external proxies inline in streamSettings.externalProxy. The startup
migration that converts those to host rows runs once and is gated off
afterwards, so it never sees a freshly imported inbound, leaving its
external proxies stranded in streamSettings (never surfaced as Hosts).

Extract the migration's per-inbound conversion into a shared
database.CreateHostsFromExternalProxy and run it inside the AddInbound
transaction. No-op for inbounds without externalProxy (everything the
current UI builds), so it only fires on such imports.
2026-06-27 13:50:06 +02:00

104 lines
3.7 KiB
Go

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)
}
}