feat: fix proxy compatibility and establish TUI foundation

This commit is contained in:
proxyctl coordinator
2026-07-17 20:30:52 +08:00
parent 0f4c122eff
commit daeb788eac
34 changed files with 1521 additions and 176 deletions
+72 -3
View File
@@ -37,7 +37,7 @@ func TestAvailabilityRequiresDebianMarkerAPTToolAndConfigurationDirectory(t *tes
mustWrite(t, filepath.Join(root, "etc", "debian_version"), "12\n")
}
runner := &fakeRunner{available: tc.tool, outputError: !tc.dumpWorks}
a := apt.New(apt.Configuration{DebianVersionFile: filepath.Join(root, "etc", "debian_version"), ConfigDirectory: configDir, OverlayFile: filepath.Join(configDir, "99proxyctl"), TrustedRoot: root}, nil, runner)
a := apt.New(apt.Configuration{DebianVersionFile: filepath.Join(root, "etc", "debian_version"), ConfigDirectory: configDir, TrustedRoot: root}, nil, runner)
got := a.Availability(context.Background())
if got.Available != tc.want || got.Evidence == "" {
t.Fatalf("availability = %#v", got)
@@ -225,6 +225,59 @@ func TestOverlayNameCollisionNeverRewritesOrRemovesUnownedConfiguration(t *testi
}
}
func TestDefaultOverlayNameSortsAfterCommonAPTFragments(t *testing.T) {
name := filepath.Base(apt.New(apt.Configuration{}, nil, nil).OverlayPath())
if filepath.Ext(name) != ".conf" {
t.Fatalf("default overlay name %q has no .conf extension; stricter APT versions ignore extensionless fragments", name)
}
for _, fragment := range []string{"proxy.conf", "99update-notifier", "01-vendor-ubuntu"} {
if strings.Compare(name, fragment) <= 0 {
t.Fatalf("default overlay name %q does not sort after common fragment %q; a later fragment shadows the overlay and enable can never become effective", name, fragment)
}
}
}
func TestOwnedLegacyOverlayIsRemovedOnEnableAndDisable(t *testing.T) {
legacyContents := "// Generated by proxyctl. Do not edit.\nAcquire::http::Proxy \"http://stale.example:3128\";\n"
t.Run("enable", func(t *testing.T) {
a, _ := newAdapter(t, nil)
legacy := filepath.Join(filepath.Dir(a.OverlayPath()), "99proxyctl")
mustWrite(t, legacy, legacyContents)
if got := a.SetDesired(context.Background(), true, compatibleProfile()); got.Err != nil || got.Inspection.State != target.Enabled {
t.Fatalf("enable = %#v", got)
}
if _, err := os.Stat(legacy); !errors.Is(err, os.ErrNotExist) {
t.Fatalf("owned legacy overlay remains after enable: %v", err)
}
})
t.Run("disable", func(t *testing.T) {
a, _ := newAdapter(t, nil)
legacy := filepath.Join(filepath.Dir(a.OverlayPath()), "99proxyctl")
mustWrite(t, legacy, legacyContents)
if got := a.SetDesired(context.Background(), false, nil); got.Err != nil {
t.Fatalf("disable = %#v", got)
}
if _, err := os.Stat(legacy); !errors.Is(err, os.ErrNotExist) {
t.Fatalf("owned legacy overlay remains after disable: %v", err)
}
})
t.Run("unowned legacy is untouched", func(t *testing.T) {
a, _ := newAdapter(t, nil)
legacy := filepath.Join(filepath.Dir(a.OverlayPath()), "99proxyctl")
original := "Acquire::http::Proxy \"http://user-owned.example:3128\";\n"
mustWrite(t, legacy, original)
if got := a.SetDesired(context.Background(), true, compatibleProfile()); got.Err != nil || got.Inspection.State != target.Enabled {
t.Fatalf("enable = %#v", got)
}
if got := readFile(t, legacy); got != original {
t.Fatalf("unowned legacy overlay changed: %q", got)
}
})
}
func TestDisablePermissionFailureReportsActualManagedState(t *testing.T) {
files := &failingFS{}
a, _ := newFailingAdapter(t, files)
@@ -268,7 +321,7 @@ func TestIncompatibleProtocolOrBypassPreventsMutationAndNamesValue(t *testing.T)
value string
}{
{"socks5 local DNS", func(p *profile.Profile) { p.HTTPURL = "socks5://unsupported.example:1080" }, "socks5://unsupported.example:1080"},
{"incompatible overridden ALL", func(p *profile.Profile) { p.AllURL = "socks5://all.example:1080" }, "socks5://all.example:1080"},
{"incompatible ALL fallback", func(p *profile.Profile) { p.HTTPURL = ""; p.AllURL = "socks5://all.example:1080" }, "socks5://all.example:1080"},
{"domain suffix", func(p *profile.Profile) { p.Bypass = []string{".example.com"} }, ".example.com"},
{"CIDR", func(p *profile.Profile) { p.Bypass = []string{"10.0.0.0/8"} }, "10.0.0.0/8"},
{"IPv6", func(p *profile.Profile) { p.Bypass = []string{"2001:db8::1"} }, "2001:db8::1"},
@@ -323,6 +376,22 @@ func TestManagedOverlayIsAtomicOwnerOnlyAndRejectsSymlinkVictims(t *testing.T) {
})
}
func TestCompleteHTTPProfileIgnoresAdditionalSOCKSProxy(t *testing.T) {
a, _ := newAdapter(t, nil)
p := &profile.Profile{
Name: "dual", Mode: profile.AdvancedMode,
HTTPURL: "http://localhost:7890", HTTPSURL: "http://localhost:7890", AllURL: "socks5://localhost:7890",
}
got := a.SetDesired(context.Background(), true, p)
if got.Err != nil || got.Inspection.State != target.Enabled {
t.Fatalf("enable = %#v", got)
}
contents := readFile(t, a.OverlayPath())
if strings.Count(contents, `"http://localhost:7890"`) != 2 || strings.Contains(contents, "socks5") {
t.Fatalf("dual-protocol translation:\n%s", contents)
}
}
func TestAdvancedALLValueSuppliesMissingAPTProtocols(t *testing.T) {
a, _ := newAdapter(t, nil)
p := &profile.Profile{Name: "all", Mode: profile.AdvancedMode, AllURL: "socks5h://proxy.example:1080"}
@@ -361,7 +430,7 @@ func newAdapter(t *testing.T, files apt.FileSystem) (*apt.Adapter, *fakeRunner)
if failure, ok := files.(*failingFS); ok {
failure.delegate = apt.NewOSFileSystem(root)
}
a := apt.New(apt.Configuration{DebianVersionFile: filepath.Join(root, "etc", "debian_version"), ConfigDirectory: dir, OverlayFile: filepath.Join(dir, "99proxyctl"), TrustedRoot: root}, files, runner)
a := apt.New(apt.Configuration{DebianVersionFile: filepath.Join(root, "etc", "debian_version"), ConfigDirectory: dir, TrustedRoot: root}, files, runner)
runner.overlay = a.OverlayPath()
return a, runner
}