128 lines
4.6 KiB
Go
128 lines
4.6 KiB
Go
package service
|
|
|
|
import (
|
|
"path/filepath"
|
|
"testing"
|
|
|
|
"github.com/mhsanaei/3x-ui/v3/internal/database"
|
|
"github.com/mhsanaei/3x-ui/v3/internal/database/model"
|
|
)
|
|
|
|
// TestTrafficStatsSwitchesPlumbing pins the B4 settings-page wiring
|
|
// end to end at the service layer: the value the settings page reads
|
|
// and writes (AllSetting.trafficStatsSwitches, persisted by
|
|
// UpdateAllSetting under the settings.trafficStatsSwitches key) is the
|
|
// same value the sing-box generator consumes
|
|
// (SingboxService.GetTrafficStatsSwitches), and the generated agent
|
|
// config carries an experimental/v2ray_api stats block whenever at
|
|
// least one category gate is on (both direction switches; all four
|
|
// off → no experimental block at all). Since the ITEM6 B5 fix, the
|
|
// block always lists inbounds, users and outbounds together — the
|
|
// core registers no counters otherwise — and the per-category
|
|
// counting policy is enforced by the master at recording time, not
|
|
// by omission here.
|
|
func TestTrafficStatsSwitchesPlumbing(t *testing.T) {
|
|
dbDir := t.TempDir()
|
|
if err := database.InitDB(filepath.Join(dbDir, "x-ui.db")); err != nil {
|
|
t.Fatalf("InitDB: %v", err)
|
|
}
|
|
defer database.CloseDB()
|
|
|
|
nodeID := 1
|
|
inbound := &model.Inbound{
|
|
Remark: "stats-plumbing",
|
|
Enable: true,
|
|
Protocol: model.VLESS,
|
|
Port: 443,
|
|
Tag: "inbound-443",
|
|
NodeID: &nodeID,
|
|
SingboxConfig: `{"type":"vless","tag":"inbound-443","listen":"::","listen_port":443,` +
|
|
`"users":[{"name":"a@example.com","uuid":"00000000-0000-0000-0000-000000000001"}]}`,
|
|
}
|
|
if err := database.GetDB().Create(inbound).Error; err != nil {
|
|
t.Fatalf("create inbound: %v", err)
|
|
}
|
|
|
|
settingService := &SettingService{}
|
|
singboxService := &SingboxService{}
|
|
|
|
statsOf := func(cfg map[string]any) map[string]any {
|
|
exp, _ := cfg["experimental"].(map[string]any)
|
|
if exp == nil {
|
|
return nil
|
|
}
|
|
api, _ := exp["v2ray_api"].(map[string]any)
|
|
stats, _ := api["stats"].(map[string]any)
|
|
return stats
|
|
}
|
|
|
|
save := func(combo string) {
|
|
t.Helper()
|
|
all, err := settingService.GetAllSetting()
|
|
if err != nil {
|
|
t.Fatalf("GetAllSetting: %v", err)
|
|
}
|
|
all.TrafficStatsSwitches = combo
|
|
if err := settingService.UpdateAllSetting(all, SecretClears{}); err != nil {
|
|
t.Fatalf("UpdateAllSetting(%s): %v", combo, err)
|
|
}
|
|
}
|
|
|
|
gen := func() map[string]any {
|
|
t.Helper()
|
|
cfg, err := singboxService.GetSingboxConfigForAgent(nodeID, "", "")
|
|
if err != nil {
|
|
t.Fatalf("GetSingboxConfigForAgent: %v", err)
|
|
}
|
|
return cfg
|
|
}
|
|
|
|
// Factory default (nothing saved yet): inbound pair on, outbound
|
|
// pair off — the settings page shows this state and the generator
|
|
// must agree.
|
|
def := singboxService.GetTrafficStatsSwitches()
|
|
if !def.InboundUplink || !def.InboundDownlink || def.OutboundUplink || def.OutboundDownlink {
|
|
t.Fatalf("factory default switches = %+v, want inbound on / outbound off", def)
|
|
}
|
|
if stats := statsOf(gen()); stats == nil || stats["inbounds"] == nil {
|
|
t.Errorf("default: stats.inbounds missing")
|
|
} else if _, ok := stats["outbounds"]; !ok {
|
|
t.Errorf("default: stats.outbounds must accompany the block (core registers no counters without all three lists)")
|
|
}
|
|
|
|
// All four off: read path returns the saved string verbatim, the
|
|
// generator parses it to all-off, and no experimental block is
|
|
// emitted.
|
|
allOff := `{"inboundUplink":false,"inboundDownlink":false,"outboundUplink":false,"outboundDownlink":false}`
|
|
save(allOff)
|
|
back, err := settingService.GetAllSetting()
|
|
if err != nil {
|
|
t.Fatalf("GetAllSetting after save: %v", err)
|
|
}
|
|
if back.TrafficStatsSwitches != allOff {
|
|
t.Errorf("GetAllSetting round-trip = %q, want %q", back.TrafficStatsSwitches, allOff)
|
|
}
|
|
if sw := singboxService.GetTrafficStatsSwitches(); sw != (trafficStatsSwitches{}) {
|
|
t.Errorf("generator switches after all-off save = %+v, want all off", sw)
|
|
}
|
|
if cfg := gen(); cfg["experimental"] != nil {
|
|
t.Errorf("all-off: experimental block generated anyway: %v", cfg["experimental"])
|
|
}
|
|
|
|
// Inbound half-on (uplink only) closes the inbound category gate;
|
|
// outbound both-on opens its gate. Either gate on → the block is
|
|
// generated with all three lists (registration requirement).
|
|
mixed := `{"inboundUplink":true,"inboundDownlink":false,"outboundUplink":true,"outboundDownlink":true}`
|
|
save(mixed)
|
|
stats := statsOf(gen())
|
|
if stats == nil {
|
|
t.Fatalf("mixed: no stats block although outbound category is on")
|
|
}
|
|
if _, ok := stats["inbounds"]; !ok {
|
|
t.Errorf("mixed: stats.inbounds must accompany the block (registration requirement; recording gate enforces the policy)")
|
|
}
|
|
if _, ok := stats["outbounds"]; !ok {
|
|
t.Errorf("mixed: outbound both-on did not generate stats.outbounds")
|
|
}
|
|
}
|