package app import ( "fmt" "os" "path/filepath" "strings" "testing" "time" ) func TestTransportSystemdProvisionWritesSingBoxTemplateAndDropIn(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, " ") switch cmd { case "systemctl daemon-reload": return "", "", 0, nil default: return "", "", 0, nil } } client := TransportClient{ ID: "sg-template", Kind: TransportClientSingBox, Config: map[string]any{ "runner": "systemd", "unit": "singbox@sg-template.service", "exec_start": "/usr/bin/sing-box run -c /tmp/sg-template.json", }, } backend := selectTransportBackend(client) res := backend.Provision(client) if !res.OK { t.Fatalf("expected provision success, got %#v", res) } templatePath := filepath.Join(tmpDir, "singbox@.service") templateData, err := os.ReadFile(templatePath) if err != nil { t.Fatalf("failed to read template unit: %v", err) } templateText := string(templateData) if !strings.Contains(templateText, transportSingBoxTemplateMarker) { t.Fatalf("template marker missing: %s", templateText) } dropInPath := filepath.Join(tmpDir, "singbox@sg-template.service.d", transportSingBoxInstanceDropIn) dropInData, err := os.ReadFile(dropInPath) if err != nil { t.Fatalf("failed to read drop-in unit: %v", err) } dropInText := string(dropInData) if !strings.Contains(dropInText, "Environment=SVPN_TRANSPORT_ID=sg-template") { t.Fatalf("drop-in ownership marker missing: %s", dropInText) } if !strings.Contains(dropInText, "ExecStart=/bin/sh -lc") { t.Fatalf("drop-in exec start missing: %s", dropInText) } } func TestTransportSystemdBackendStartAutoProvisionOnMissingTemplateInstance(t *testing.T) { origRunner := transportRunCommand origUnitsDir := transportSystemdUnitsDir defer func() { transportRunCommand = origRunner transportSystemdUnitsDir = origUnitsDir }() tmpDir := t.TempDir() transportSystemdUnitsDir = tmpDir startCalls := 0 transportRunCommand = func(_ time.Duration, name string, args ...string) (string, string, int, error) { cmd := name + " " + strings.Join(args, " ") switch cmd { case "systemctl reset-failed singbox@sg-auto.service": return "", "", 0, nil case "systemctl daemon-reload": return "", "", 0, nil case "systemctl start singbox@sg-auto.service": startCalls++ if startCalls == 1 { return "", "Failed to start singbox@sg-auto.service: Unit singbox@sg-auto.service not found.", 5, fmt.Errorf("exit status 5") } return "", "", 0, nil default: return "", "", 0, nil } } client := TransportClient{ ID: "sg-auto", Kind: TransportClientSingBox, Config: map[string]any{ "runner": "systemd", "unit": "singbox@sg-auto.service", "exec_start": "/usr/bin/sing-box run -c /tmp/sg-auto.json", }, } backend := selectTransportBackend(client) res := backend.Action(client, "start") if !res.OK { t.Fatalf("expected start success after auto-provision, got %#v", res) } if startCalls != 2 { t.Fatalf("expected start retried after auto-provision, got calls=%d", startCalls) } if _, err := os.Stat(filepath.Join(tmpDir, "singbox@.service")); err != nil { t.Fatalf("expected template unit file, stat error: %v", err) } if _, err := os.Stat(filepath.Join(tmpDir, "singbox@sg-auto.service.d", transportSingBoxInstanceDropIn)); err != nil { t.Fatalf("expected instance drop-in file, stat error: %v", err) } } func TestTransportSystemdCleanupRemovesSingBoxDropInKeepsTemplate(t *testing.T) { origRunner := transportRunCommand origUnitsDir := transportSystemdUnitsDir defer func() { transportRunCommand = origRunner transportSystemdUnitsDir = origUnitsDir }() tmpDir := t.TempDir() transportSystemdUnitsDir = tmpDir templatePath := filepath.Join(tmpDir, "singbox@.service") if err := os.WriteFile(templatePath, []byte(transportSingBoxTemplateMarker+"\n[Service]\nExecStart=/bin/true\n"), 0o644); err != nil { t.Fatalf("write template unit file: %v", err) } dropInDir := filepath.Join(tmpDir, "singbox@sg-clean.service.d") if err := os.MkdirAll(dropInDir, 0o755); err != nil { t.Fatalf("mkdir drop-in dir: %v", err) } dropInPath := filepath.Join(dropInDir, transportSingBoxInstanceDropIn) if err := os.WriteFile(dropInPath, []byte("[Service]\nEnvironment=SVPN_TRANSPORT_ID=sg-clean\n"), 0o644); err != nil { t.Fatalf("write drop-in file: %v", err) } calls := make([]string, 0, 8) transportRunCommand = func(_ time.Duration, name string, args ...string) (string, string, int, error) { cmd := name + " " + strings.Join(args, " ") calls = append(calls, cmd) return "", "", 0, nil } client := TransportClient{ ID: "sg-clean", Kind: TransportClientSingBox, Config: map[string]any{ "runner": "systemd", "unit": "singbox@sg-clean.service", }, } backend := selectTransportBackend(client) res := backend.Cleanup(client) if !res.OK { t.Fatalf("expected cleanup success, got %#v", res) } if _, err := os.Stat(templatePath); err != nil { t.Fatalf("template should stay intact, stat error: %v", err) } if _, err := os.Stat(dropInPath); !os.IsNotExist(err) { t.Fatalf("drop-in file was not removed: %v", err) } expected := []string{ "systemctl stop singbox@sg-clean.service", "systemctl disable singbox@sg-clean.service", "systemctl daemon-reload", "systemctl reset-failed singbox@sg-clean.service", } for _, want := range expected { found := false for _, got := range calls { if got == want { found = true break } } if !found { t.Fatalf("missing cleanup call %q in %#v", want, calls) } } } func TestTransportSystemdProvisionSingBoxTemplateIncludesNetnsEnv(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, " ") switch cmd { case "systemctl daemon-reload": return "", "", 0, nil default: return "", "", 0, nil } } client := TransportClient{ ID: "sg-netns", Kind: TransportClientSingBox, Config: map[string]any{ "runner": "systemd", "unit": "singbox@sg-netns.service", "exec_start": "/usr/bin/sing-box run -c /tmp/sg-netns.json", "netns_enabled": true, "netns_name": "svpn-sg-netns", }, } backend := selectTransportBackend(client) res := backend.Provision(client) if !res.OK { t.Fatalf("expected provision success, got %#v", res) } dropInPath := filepath.Join(tmpDir, "singbox@sg-netns.service.d", transportSingBoxInstanceDropIn) dropInData, err := os.ReadFile(dropInPath) if err != nil { t.Fatalf("failed to read drop-in unit: %v", err) } dropInText := string(dropInData) for _, want := range []string{ "Environment=SVPN_NETNS_ENABLED=1", "Environment=SVPN_NETNS_NAME=svpn-sg-netns", } { if !strings.Contains(dropInText, want) { t.Fatalf("drop-in is missing %q: %s", want, dropInText) } } } func TestTransportSystemdPreActionMigratesOwnedLegacyUnitToTemplateInstance(t *testing.T) { origRunner := transportRunCommand origUnitsDir := transportSystemdUnitsDir defer func() { transportRunCommand = origRunner transportSystemdUnitsDir = origUnitsDir }() tmpDir := t.TempDir() transportSystemdUnitsDir = tmpDir legacyUnit := "singbox-sg-migrate.service" legacyPath := filepath.Join(tmpDir, legacyUnit) legacyBody := "[Service]\nEnvironment=SVPN_TRANSPORT_ID=sg-migrate\nExecStart=/bin/true\n" if err := os.WriteFile(legacyPath, []byte(legacyBody), 0o644); err != nil { t.Fatalf("write legacy unit: %v", err) } calls := make([]string, 0, 10) transportRunCommand = func(_ time.Duration, name string, args ...string) (string, string, int, error) { cmd := name + " " + strings.Join(args, " ") calls = append(calls, cmd) return "", "", 0, nil } client := TransportClient{ ID: "sg-migrate", Kind: TransportClientSingBox, Config: map[string]any{ "runner": "systemd", "unit": "singbox@.service", "bootstrap_bypass": false, }, } backend := selectTransportBackend(client) res := backend.Action(client, "start") if !res.OK { t.Fatalf("expected start success, got %#v", res) } if !strings.Contains(res.Stdout, "legacy-migrate: singbox-sg-migrate.service -> singbox@sg-migrate.service") { t.Fatalf("expected migrate message in stdout, got: %s", res.Stdout) } if _, err := os.Stat(legacyPath); !os.IsNotExist(err) { t.Fatalf("legacy unit must be removed after migration, stat=%v", err) } mustContain := []string{ "systemctl stop singbox-sg-migrate.service", "systemctl disable singbox-sg-migrate.service", "systemctl daemon-reload", "systemctl reset-failed singbox-sg-migrate.service", "systemctl reset-failed singbox@sg-migrate.service", "systemctl start singbox@sg-migrate.service", } got := strings.Join(calls, " | ") for _, want := range mustContain { if !strings.Contains(got, want) { t.Fatalf("missing call %q in %s", want, got) } } } func TestTransportSystemdPreActionLegacyMigrationDryRun(t *testing.T) { origRunner := transportRunCommand origUnitsDir := transportSystemdUnitsDir defer func() { transportRunCommand = origRunner transportSystemdUnitsDir = origUnitsDir }() tmpDir := t.TempDir() transportSystemdUnitsDir = tmpDir legacyUnit := "singbox-sg-dry.service" legacyPath := filepath.Join(tmpDir, legacyUnit) legacyBody := "[Service]\nEnvironment=SVPN_TRANSPORT_ID=sg-dry\nExecStart=/bin/true\n" if err := os.WriteFile(legacyPath, []byte(legacyBody), 0o644); err != nil { t.Fatalf("write legacy unit: %v", err) } calls := make([]string, 0, 10) transportRunCommand = func(_ time.Duration, name string, args ...string) (string, string, int, error) { cmd := name + " " + strings.Join(args, " ") calls = append(calls, cmd) return "", "", 0, nil } client := TransportClient{ ID: "sg-dry", Kind: TransportClientSingBox, Config: map[string]any{ "runner": "systemd", "unit": "singbox@.service", "bootstrap_bypass": false, transportSingBoxLegacyMigrateDryRunConfigKey: true, }, } backend := selectTransportBackend(client) res := backend.Action(client, "start") if !res.OK { t.Fatalf("expected start success, got %#v", res) } if !strings.Contains(res.Stdout, "legacy-migrate dry-run: singbox-sg-dry.service -> singbox@sg-dry.service") { t.Fatalf("expected dry-run message in stdout, got: %s", res.Stdout) } if _, err := os.Stat(legacyPath); err != nil { t.Fatalf("legacy unit must stay on dry-run, stat=%v", err) } got := strings.Join(calls, " | ") if strings.Contains(got, "systemctl stop singbox-sg-dry.service") { t.Fatalf("dry-run must not stop legacy unit, calls=%s", got) } if strings.Contains(got, "systemctl disable singbox-sg-dry.service") { t.Fatalf("dry-run must not disable legacy unit, calls=%s", got) } if !strings.Contains(got, "systemctl start singbox@sg-dry.service") { t.Fatalf("expected template start call, got=%s", got) } } func TestTransportSystemdPreActionLegacyMigrationSkipsForeignOwnership(t *testing.T) { origRunner := transportRunCommand origUnitsDir := transportSystemdUnitsDir defer func() { transportRunCommand = origRunner transportSystemdUnitsDir = origUnitsDir }() tmpDir := t.TempDir() transportSystemdUnitsDir = tmpDir legacyUnit := "singbox-sg-foreign.service" legacyPath := filepath.Join(tmpDir, legacyUnit) legacyBody := "[Service]\nEnvironment=SVPN_TRANSPORT_ID=other-client\nExecStart=/bin/true\n" if err := os.WriteFile(legacyPath, []byte(legacyBody), 0o644); err != nil { t.Fatalf("write legacy unit: %v", err) } calls := make([]string, 0, 10) transportRunCommand = func(_ time.Duration, name string, args ...string) (string, string, int, error) { cmd := name + " " + strings.Join(args, " ") calls = append(calls, cmd) return "", "", 0, nil } client := TransportClient{ ID: "sg-foreign", Kind: TransportClientSingBox, Config: map[string]any{ "runner": "systemd", "unit": "singbox@.service", "bootstrap_bypass": false, }, } backend := selectTransportBackend(client) res := backend.Action(client, "start") if !res.OK { t.Fatalf("expected start success, got %#v", res) } if _, err := os.Stat(legacyPath); err != nil { t.Fatalf("foreign legacy unit must stay untouched, stat=%v", err) } got := strings.Join(calls, " | ") if strings.Contains(got, "systemctl stop singbox-sg-foreign.service") { t.Fatalf("must not stop foreign legacy unit, calls=%s", got) } if strings.Contains(got, "systemctl disable singbox-sg-foreign.service") { t.Fatalf("must not disable foreign legacy unit, calls=%s", got) } if !strings.Contains(got, "systemctl start singbox@sg-foreign.service") { t.Fatalf("expected template start call, got=%s", got) } } func TestTransportSystemdCleanupTemplateRemovesOwnedLegacyUnits(t *testing.T) { origRunner := transportRunCommand origUnitsDir := transportSystemdUnitsDir defer func() { transportRunCommand = origRunner transportSystemdUnitsDir = origUnitsDir }() tmpDir := t.TempDir() transportSystemdUnitsDir = tmpDir templatePath := filepath.Join(tmpDir, "singbox@.service") if err := os.WriteFile(templatePath, []byte(transportSingBoxTemplateMarker+"\n[Service]\nExecStart=/bin/true\n"), 0o644); err != nil { t.Fatalf("write template unit: %v", err) } dropInDir := filepath.Join(tmpDir, "singbox@sg-clean-mixed.service.d") if err := os.MkdirAll(dropInDir, 0o755); err != nil { t.Fatalf("mkdir drop-in dir: %v", err) } dropInPath := filepath.Join(dropInDir, transportSingBoxInstanceDropIn) if err := os.WriteFile(dropInPath, []byte("[Service]\nEnvironment=SVPN_TRANSPORT_ID=sg-clean-mixed\n"), 0o644); err != nil { t.Fatalf("write drop-in: %v", err) } legacyPath := filepath.Join(tmpDir, "singbox-sg-clean-mixed.service") if err := os.WriteFile(legacyPath, []byte("[Service]\nEnvironment=SVPN_TRANSPORT_ID=sg-clean-mixed\nExecStart=/bin/true\n"), 0o644); err != nil { t.Fatalf("write legacy unit: %v", err) } calls := make([]string, 0, 16) transportRunCommand = func(_ time.Duration, name string, args ...string) (string, string, int, error) { cmd := name + " " + strings.Join(args, " ") calls = append(calls, cmd) return "", "", 0, nil } client := TransportClient{ ID: "sg-clean-mixed", Kind: TransportClientSingBox, Config: map[string]any{ "runner": "systemd", "unit": "singbox@.service", }, } backend := selectTransportBackend(client) res := backend.Cleanup(client) if !res.OK { t.Fatalf("expected cleanup success, got %#v", res) } if _, err := os.Stat(templatePath); err != nil { t.Fatalf("template must stay intact, stat err=%v", err) } if _, err := os.Stat(dropInPath); !os.IsNotExist(err) { t.Fatalf("drop-in must be removed, stat=%v", err) } if _, err := os.Stat(legacyPath); !os.IsNotExist(err) { t.Fatalf("legacy unit must be removed, stat=%v", err) } got := strings.Join(calls, " | ") mustContain := []string{ "systemctl stop singbox@sg-clean-mixed.service", "systemctl disable singbox@sg-clean-mixed.service", "systemctl stop singbox-sg-clean-mixed.service", "systemctl disable singbox-sg-clean-mixed.service", "systemctl daemon-reload", "systemctl reset-failed singbox@sg-clean-mixed.service", "systemctl reset-failed singbox-sg-clean-mixed.service", } for _, want := range mustContain { if !strings.Contains(got, want) { t.Fatalf("missing cleanup call %q in %s", want, got) } } }