package app import ( "encoding/json" "os" "path/filepath" "strings" "testing" "time" ) func TestTransportMigrateSingBoxDNSConfigMapLegacyUDP(t *testing.T) { root := map[string]any{ "dns": map[string]any{ "servers": []any{ map[string]any{ "tag": "dns-direct", "address": "1.1.1.1", "address_resolver": "dns-local", "address_strategy": "prefer_ipv4", }, }, }, } changed, warns := transportMigrateSingBoxDNSConfigMap(root) if !changed { t.Fatalf("expected changed=true") } if len(warns) != 0 { t.Fatalf("unexpected warnings: %#v", warns) } dns := root["dns"].(map[string]any) servers := dns["servers"].([]any) srv := servers[0].(map[string]any) if strings.TrimSpace(asString(srv["type"])) != "udp" { t.Fatalf("expected type=udp, got %#v", srv["type"]) } if strings.TrimSpace(asString(srv["server"])) != "1.1.1.1" { t.Fatalf("expected server=1.1.1.1, got %#v", srv["server"]) } if _, ok := srv["address"]; ok { t.Fatalf("legacy address key must be removed") } if strings.TrimSpace(asString(srv["domain_resolver"])) != "dns-local" { t.Fatalf("expected domain_resolver migration, got %#v", srv["domain_resolver"]) } if strings.TrimSpace(asString(srv["domain_strategy"])) != "prefer_ipv4" { t.Fatalf("expected domain_strategy migration, got %#v", srv["domain_strategy"]) } if _, ok := srv["detour"]; ok { t.Fatalf("detour=direct should be removed in migrated config") } } func TestTransportMigrateSingBoxDNSConfigMapHTTPS(t *testing.T) { root := map[string]any{ "dns": map[string]any{ "servers": []any{ map[string]any{ "tag": "dns-doh", "address": "https://1.1.1.1/dns-query", }, }, }, } changed, warns := transportMigrateSingBoxDNSConfigMap(root) if !changed { t.Fatalf("expected changed=true") } if len(warns) != 0 { t.Fatalf("unexpected warnings: %#v", warns) } dns := root["dns"].(map[string]any) srv := dns["servers"].([]any)[0].(map[string]any) if strings.TrimSpace(asString(srv["type"])) != "https" { t.Fatalf("expected type=https, got %#v", srv["type"]) } if strings.TrimSpace(asString(srv["server"])) != "1.1.1.1" { t.Fatalf("expected server=1.1.1.1, got %#v", srv["server"]) } if _, ok := srv["path"]; ok { t.Fatalf("default /dns-query path should not be forced into config") } } func TestTransportMigrateSingBoxDNSConfigMapTypedServerDetourDirectRemoved(t *testing.T) { root := map[string]any{ "dns": map[string]any{ "servers": []any{ map[string]any{ "tag": "dns-direct", "type": "udp", "server": "1.1.1.1", "detour": "direct", }, }, }, } changed, warns := transportMigrateSingBoxDNSConfigMap(root) if !changed { t.Fatalf("expected changed=true when removing direct detour") } if len(warns) != 0 { t.Fatalf("unexpected warnings: %#v", warns) } dns := root["dns"].(map[string]any) srv := dns["servers"].([]any)[0].(map[string]any) if _, ok := srv["detour"]; ok { t.Fatalf("detour should be removed for typed DNS server") } } func TestTransportSystemdBackendProvisionSingBoxDNSMigrationStrictFail(t *testing.T) { origRunner := transportRunCommand origUnitsDir := transportSystemdUnitsDir defer func() { transportRunCommand = origRunner transportSystemdUnitsDir = origUnitsDir }() transportSystemdUnitsDir = t.TempDir() transportRunCommand = func(_ time.Duration, _ string, _ ...string) (string, string, int, error) { return "", "", 0, nil } cfgDir := t.TempDir() cfg := filepath.Join(cfgDir, "singbox.json") if err := os.WriteFile(cfg, []byte("{broken-json"), 0o644); err != nil { t.Fatalf("write config: %v", err) } client := TransportClient{ ID: "sg-mig-strict", Kind: TransportClientSingBox, Config: map[string]any{ "runner": "systemd", "unit": "sg-mig-strict.service", "bin": "/usr/bin/sing-box", "singbox_config_path": cfg, "singbox_dns_migrate_legacy": true, "singbox_dns_migrate_strict": true, }, } res := selectTransportBackend(client).Provision(client) if res.OK { t.Fatalf("expected strict migration failure, got %#v", res) } if res.Code != "TRANSPORT_BACKEND_SINGBOX_DNS_MIGRATE_FAILED" { t.Fatalf("unexpected code: %#v", res) } } func TestTransportSystemdBackendProvisionSingBoxDNSMigrationWritesBackup(t *testing.T) { origRunner := transportRunCommand origUnitsDir := transportSystemdUnitsDir defer func() { transportRunCommand = origRunner transportSystemdUnitsDir = origUnitsDir }() tmpDir := t.TempDir() transportSystemdUnitsDir = tmpDir transportRunCommand = func(_ time.Duration, name string, args ...string) (string, string, int, error) { cmd := name + " " + strings.Join(args, " ") if cmd == "systemctl daemon-reload" { return "", "", 0, nil } return "", "", 0, nil } cfgDir := t.TempDir() cfg := filepath.Join(cfgDir, "singbox.json") legacy := map[string]any{ "dns": map[string]any{ "servers": []any{ map[string]any{ "tag": "dns-direct", "address": "1.1.1.1", }, }, }, } raw, _ := json.Marshal(legacy) if err := os.WriteFile(cfg, raw, 0o644); err != nil { t.Fatalf("write config: %v", err) } client := TransportClient{ ID: "sg-mig-ok", Kind: TransportClientSingBox, Config: map[string]any{ "runner": "systemd", "unit": "sg-mig-ok.service", "bin": "/usr/bin/sing-box", "singbox_config_path": cfg, "singbox_dns_migrate_legacy": true, }, } res := selectTransportBackend(client).Provision(client) if !res.OK { t.Fatalf("expected provision success, got %#v", res) } if !strings.Contains(strings.ToLower(res.Stdout), "dns-migrate:") { t.Fatalf("expected migration note in stdout, got %#v", res.Stdout) } if _, err := os.Stat(cfg + ".legacy-dns.bak"); err != nil { t.Fatalf("expected backup file, stat err=%v", err) } updatedRaw, err := os.ReadFile(cfg) if err != nil { t.Fatalf("read updated config: %v", err) } var updated map[string]any if err := json.Unmarshal(updatedRaw, &updated); err != nil { t.Fatalf("parse updated config: %v", err) } dns := updated["dns"].(map[string]any) srv := dns["servers"].([]any)[0].(map[string]any) if strings.TrimSpace(asString(srv["type"])) != "udp" { t.Fatalf("expected migrated type=udp, got %#v", srv["type"]) } if _, ok := srv["address"]; ok { t.Fatalf("legacy address key should be removed after migration") } }