- service/xray.go 1741->573行: 只剩 agent 配置构建(GetXrayConfigForAgent/buildAgentXrayConfig=模板+节点入站+egress+转发编译); 删进程生命周期/状态机/RestartXray/StopXray/SetToNeedRestart/GetXrayTraffic/GetOnlineUsers/balancer/TestRoute 与 injectPanelEgress/injectNodeEgresses/injectMtproto/injectAmneziawg/mergeSubscriptionOutbounds
- controller: 43处 SetToNeedRestart 与各 RestartXray 调用清除; 删 balancerStatus/balancerOverride/routeTest/getXrayResult 路由与handler(纯本地核心操作); 前端删 RouteTester 页签与组件
- server service: GetConfigJson 改模板直读; 状态Xray块恒 stopped/空版本; Stop/RestartXrayService 改 no-op(DB导入/恢复流程保留形状)
- setting service: PanelEgressProxyURL/NodeEgressProxyURL 恒直连(桥在本地核心内,已不存在); NodeService.withOutboundBridge 恒直连回退
- runtime: Local 改为全 no-op 桩(DB即真源); NewManager() 去 LocalDeps(APIPort/SetNeedRestart)
- job: 删 XrayTrafficJob/CheckXrayRunningJob/CheckClientIpJob 三文件(共享声明 EventBus/IPWithTimestamp/clientStatsSnapshotMaxClients 迁 shared.go); web.go 不再调度 MtprotoJob/AmneziaWGJob, 删 OnCrash 接线与 stop() 的 sidecar StopAll
- MTProto/AmneziaWG 下线: AddInbound/UpdateInbound 对两协议直接拒绝(存量行保留可删); TG bot 的 restart 命令与在线查询按无本地核心收口
- 测试: GetXrayConfig 系列移植到 agent 构建(finalmask/clients/wireguard/scale); 删退役行为测试(lifecycle/restart/mtproto本地应用/client-ip job/inject系列); 共享测试helper迁 mtproto_test_helpers_test.go 与 job/integration_helpers_test.go
- 既有红(非本轮引入,已在 f6d6520 复现): TestGetXrayConfig_EnabledClientsStillEmitted、TestRouteRegistryContract(P6删 endpoints.ts 遗留)
140 lines
4.9 KiB
Go
140 lines
4.9 KiB
Go
package service
|
|
|
|
import (
|
|
"encoding/json"
|
|
"os"
|
|
"testing"
|
|
|
|
xuilogger "github.com/mhsanaei/3x-ui/v3/internal/logger"
|
|
"github.com/mhsanaei/3x-ui/v3/internal/util/json_util"
|
|
"github.com/mhsanaei/3x-ui/v3/internal/xray"
|
|
|
|
"github.com/op/go-logging"
|
|
)
|
|
|
|
func TestMain(m *testing.M) {
|
|
// The package logger must exist before any test exercises a path that
|
|
// logs a skip/warning.
|
|
xuilogger.InitLogger(logging.ERROR)
|
|
os.Exit(m.Run())
|
|
}
|
|
|
|
func TestEnsureAPIServices(t *testing.T) {
|
|
// legacy template without RoutingService gets it injected
|
|
out := ensureAPIServices(json_util.RawMessage(`{"services":["HandlerService","LoggerService","StatsService"],"tag":"api"}`))
|
|
var parsed struct {
|
|
Services []string `json:"services"`
|
|
Tag string `json:"tag"`
|
|
}
|
|
if err := json.Unmarshal(out, &parsed); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
want := map[string]bool{"HandlerService": true, "StatsService": true, "RoutingService": true, "LoggerService": true}
|
|
if len(parsed.Services) != 4 {
|
|
t.Fatalf("expected 4 services, got %v", parsed.Services)
|
|
}
|
|
for _, svc := range parsed.Services {
|
|
if !want[svc] {
|
|
t.Fatalf("unexpected service %q", svc)
|
|
}
|
|
}
|
|
if parsed.Tag != "api" {
|
|
t.Fatalf("tag must be preserved, got %q", parsed.Tag)
|
|
}
|
|
|
|
// complete api block is returned unchanged (no marshal churn)
|
|
full := json_util.RawMessage(`{"services":["HandlerService","StatsService","RoutingService"],"tag":"api"}`)
|
|
if got := ensureAPIServices(full); string(got) != string(full) {
|
|
t.Fatalf("complete api block must pass through untouched, got %s", got)
|
|
}
|
|
|
|
// absent api block stays absent
|
|
if got := ensureAPIServices(nil); got != nil {
|
|
t.Fatalf("nil api block must stay nil, got %s", got)
|
|
}
|
|
}
|
|
|
|
func TestEnsureStatsPolicy(t *testing.T) {
|
|
// default-template shape: level "0" exists with traffic flags — the online
|
|
// flag is added and the siblings survive untouched
|
|
out := ensureStatsPolicy(json_util.RawMessage(`{"levels":{"0":{"handshake":4,"statsUserUplink":true,"statsUserDownlink":true}},"system":{"statsInboundDownlink":true}}`))
|
|
var parsed struct {
|
|
Levels map[string]map[string]any `json:"levels"`
|
|
System map[string]any `json:"system"`
|
|
}
|
|
if err := json.Unmarshal(out, &parsed); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
level0 := parsed.Levels["0"]
|
|
if level0["statsUserOnline"] != true {
|
|
t.Fatalf("statsUserOnline must be injected into level 0, got %v", level0)
|
|
}
|
|
if level0["statsUserUplink"] != true || level0["statsUserDownlink"] != true || level0["handshake"] != float64(4) {
|
|
t.Fatalf("sibling keys must be preserved, got %v", level0)
|
|
}
|
|
if parsed.System["statsInboundDownlink"] != true {
|
|
t.Fatalf("system block must be preserved, got %v", parsed.System)
|
|
}
|
|
|
|
// missing levels block: level "0" is created with the flag
|
|
out = ensureStatsPolicy(json_util.RawMessage(`{"system":{}}`))
|
|
if err := json.Unmarshal(out, &parsed); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if parsed.Levels["0"]["statsUserOnline"] != true {
|
|
t.Fatalf("level 0 must be created with statsUserOnline, got %s", out)
|
|
}
|
|
|
|
// every level gets the flag, an explicit false included — the flag is
|
|
// panel infrastructure, like the api services
|
|
out = ensureStatsPolicy(json_util.RawMessage(`{"levels":{"0":{"statsUserOnline":false},"1":{"connIdle":300}}}`))
|
|
if err := json.Unmarshal(out, &parsed); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, key := range []string{"0", "1"} {
|
|
if parsed.Levels[key]["statsUserOnline"] != true {
|
|
t.Fatalf("level %s must have statsUserOnline forced on, got %s", key, out)
|
|
}
|
|
}
|
|
if parsed.Levels["1"]["connIdle"] != float64(300) {
|
|
t.Fatalf("level 1 siblings must be preserved, got %s", out)
|
|
}
|
|
|
|
// already-enabled input passes through byte-identical (no marshal churn,
|
|
// no spurious restart)
|
|
full := json_util.RawMessage(`{"levels":{"0":{"statsUserOnline":true}}}`)
|
|
if got := ensureStatsPolicy(full); string(got) != string(full) {
|
|
t.Fatalf("already-enabled policy must pass through untouched, got %s", got)
|
|
}
|
|
|
|
// absent policy block stays absent
|
|
if got := ensureStatsPolicy(nil); got != nil {
|
|
t.Fatalf("nil policy must stay nil, got %s", got)
|
|
}
|
|
|
|
// unparsable policy is left untouched
|
|
bad := json_util.RawMessage(`{not json`)
|
|
if got := ensureStatsPolicy(bad); string(got) != string(bad) {
|
|
t.Fatalf("unparsable policy must be left untouched, got %s", got)
|
|
}
|
|
}
|
|
|
|
func egressTestConfig() *xray.Config {
|
|
return &xray.Config{
|
|
RouterConfig: json_util.RawMessage(`{"domainStrategy":"AsIs","rules":[{"type":"field","inboundTag":["api"],"outboundTag":"api"}]}`),
|
|
OutboundConfigs: json_util.RawMessage(`[{"protocol":"freedom","tag":"direct"},{"protocol":"socks","tag":"warp"}]`),
|
|
InboundConfigs: []xray.InboundConfig{
|
|
{Port: 62789, Protocol: "tunnel", Tag: "api", Listen: json_util.RawMessage(`"127.0.0.1"`)},
|
|
},
|
|
}
|
|
}
|
|
|
|
type egressRouting struct {
|
|
DomainStrategy string `json:"domainStrategy"`
|
|
Rules []struct {
|
|
InboundTag []string `json:"inboundTag"`
|
|
OutboundTag string `json:"outboundTag"`
|
|
Type string `json:"type"`
|
|
} `json:"rules"`
|
|
}
|