fix: address protocol probe review findings

This commit is contained in:
proxyctl coordinator
2026-07-18 00:44:10 +08:00
parent 02b49d5112
commit 954e7d977a
12 changed files with 315 additions and 489 deletions
-346
View File
@@ -1,15 +1,12 @@
package tui
import (
"context"
"errors"
"strings"
"testing"
tea "github.com/charmbracelet/bubbletea"
"proxyctl/internal/app"
"proxyctl/internal/probe"
"proxyctl/internal/profile"
)
@@ -138,349 +135,6 @@ func TestNoProfileStateStaysOnAccessibleMainPage(t *testing.T) {
}
}
func TestProtocolProbeDetectsHTTPAndRewritesSingleURL(t *testing.T) {
module := &recordingModule{}
model := NewModel(module)
var probed []string
model.detect = func(_ context.Context, address string) (probe.Capabilities, error) {
probed = append(probed, address)
return probe.Capabilities{HTTP: true}, nil
}
model = updateKey(model, "m")
model = updateKey(model, "n")
model = updateRunes(model, "home")
model = updateKey(model, "tab")
model = updateRunes(model, "alice:secret@localhost:8888")
model, cmd := pressCtrlD(model)
if cmd == nil {
t.Fatal("ctrl+d did not start a Protocol Probe")
}
updated, _ := model.Update(cmd())
model = updated.(Model)
if len(probed) != 1 || probed[0] != "localhost:8888" {
t.Fatalf("Protocol Probe dialed %#v, want exactly localhost:8888 without userinfo", probed)
}
if got := model.form.profile.SingleURL; got != "http://alice:secret@localhost:8888" {
t.Fatalf("Single URL after probe = %q, want detected scheme with preserved userinfo", got)
}
view := model.View()
if !strings.Contains(view, "Protocol Probe: detected HTTP proxy at localhost:8888") {
t.Fatalf("form view missing detection status:\n%s", view)
}
if strings.Contains(view, "secret") {
t.Fatalf("form view exposed proxy password:\n%s", view)
}
}
func pressCtrlD(model Model) (Model, tea.Cmd) {
updated, cmd := model.Update(tea.KeyMsg{Type: tea.KeyCtrlD})
return updated.(Model), cmd
}
func TestProtocolProbeFailureLeavesFieldUntouchedAndSavePossible(t *testing.T) {
module := &recordingModule{}
model := NewModel(module)
model.detect = func(_ context.Context, address string) (probe.Capabilities, error) {
return probe.Capabilities{}, errors.New("connection refused")
}
model = updateKey(model, "m")
model = updateKey(model, "n")
model = updateRunes(model, "home")
model = updateKey(model, "tab")
model = updateRunes(model, "localhost:8888")
model, cmd := pressCtrlD(model)
if cmd == nil {
t.Fatal("ctrl+d did not start a Protocol Probe")
}
updated, _ := model.Update(cmd())
model = updated.(Model)
if got := model.form.profile.SingleURL; got != "localhost:8888" {
t.Fatalf("failed probe rewrote Single URL to %q", got)
}
if view := model.View(); !strings.Contains(view, "Protocol Probe failed for localhost:8888") || strings.Contains(view, "connection refused") {
t.Fatalf("form view missing credential-safe field failure status:\n%s", view)
}
model = updateKey(model, "ctrl+s")
if len(module.intents) != 1 || module.intents[0].Kind != app.CreateProfile {
t.Fatalf("save after failed probe dispatched %#v", module.intents)
}
}
func TestProtocolProbeUnrecognizedProtocolLeavesFieldUntouched(t *testing.T) {
module := &recordingModule{}
model := NewModel(module)
model.detect = func(_ context.Context, address string) (probe.Capabilities, error) {
return probe.Capabilities{}, nil
}
model = updateKey(model, "m")
model = updateKey(model, "n")
model = updateRunes(model, "home")
model = updateKey(model, "tab")
model = updateRunes(model, "localhost:8888")
model, cmd := pressCtrlD(model)
if cmd == nil {
t.Fatal("ctrl+d did not start a Protocol Probe")
}
updated, _ := model.Update(cmd())
model = updated.(Model)
if got := model.form.profile.SingleURL; got != "localhost:8888" {
t.Fatalf("unrecognized probe rewrote Single URL to %q", got)
}
if view := model.View(); !strings.Contains(view, "Protocol Probe: unrecognized protocol at localhost:8888") {
t.Fatalf("form view missing unrecognized status:\n%s", view)
}
}
func TestProtocolProbeIgnoresRepeatedCtrlDWhileInFlight(t *testing.T) {
module := &recordingModule{}
model := NewModel(module)
calls := 0
model.detect = func(_ context.Context, address string) (probe.Capabilities, error) {
calls++
return probe.Capabilities{HTTP: true}, nil
}
model = updateKey(model, "m")
model = updateKey(model, "n")
model = updateRunes(model, "home")
model = updateKey(model, "tab")
model = updateRunes(model, "localhost:8888")
model, first := pressCtrlD(model)
if first == nil {
t.Fatal("ctrl+d did not start a Protocol Probe")
}
model, second := pressCtrlD(model)
if second != nil {
t.Fatal("ctrl+d during an in-flight probe started a second one")
}
updated, _ := model.Update(first())
model = updated.(Model)
if calls != 1 {
t.Fatalf("probe ran %d times, want 1", calls)
}
if got := model.form.profile.SingleURL; got != "http://localhost:8888" {
t.Fatalf("Single URL after probe = %q", got)
}
}
func TestProtocolProbeLateResultIgnoredAfterEscLeavesForm(t *testing.T) {
module := &recordingModule{}
model := NewModel(module)
model.detect = func(_ context.Context, address string) (probe.Capabilities, error) {
return probe.Capabilities{HTTP: true}, nil
}
model = updateKey(model, "m")
model = updateKey(model, "n")
model = updateRunes(model, "home")
model = updateKey(model, "tab")
model = updateRunes(model, "localhost:8888")
model, cmd := pressCtrlD(model)
if cmd == nil {
t.Fatal("ctrl+d did not start a Protocol Probe")
}
model = updateKey(model, "esc")
updated, _ := model.Update(cmd())
model = updated.(Model)
if model.page != profilesPage {
t.Fatalf("page = %d, want profilesPage", model.page)
}
if strings.Contains(model.status, "detected HTTP proxy") {
t.Fatalf("profiles page status mutated by late probe result: %s", model.status)
}
if model.form.profile.SingleURL != "" {
t.Fatalf("abandoned form SingleURL mutated to %q", model.form.profile.SingleURL)
}
}
func TestProtocolProbeLateResultIgnoredAfterSaveLeavesForm(t *testing.T) {
module := &recordingModule{}
model := NewModel(module)
model.detect = func(_ context.Context, address string) (probe.Capabilities, error) {
return probe.Capabilities{HTTP: true}, nil
}
model = updateKey(model, "m")
model = updateKey(model, "n")
model = updateRunes(model, "home")
model = updateKey(model, "tab")
model = updateRunes(model, "localhost:8888")
model, cmd := pressCtrlD(model)
if cmd == nil {
t.Fatal("ctrl+d did not start a Protocol Probe")
}
model = updateKey(model, "ctrl+s")
if len(module.intents) != 1 {
t.Fatalf("save dispatched %d intents, want 1", len(module.intents))
}
updated, _ := model.Update(cmd())
model = updated.(Model)
if model.page != profilesPage {
t.Fatalf("page = %d, want profilesPage", model.page)
}
if strings.Contains(model.status, "detected HTTP proxy") {
t.Fatalf("profiles page status mutated by late probe result: %s", model.status)
}
if model.form.profile.SingleURL != "" {
t.Fatalf("abandoned form SingleURL mutated to %q", model.form.profile.SingleURL)
}
}
func TestSaveFormDoesNotTriggerProtocolProbe(t *testing.T) {
module := &recordingModule{}
model := NewModel(module)
calls := 0
model.detect = func(_ context.Context, address string) (probe.Capabilities, error) {
calls++
return probe.Capabilities{HTTP: true}, nil
}
model = updateKey(model, "m")
model = updateKey(model, "n")
model = updateRunes(model, "home")
model = updateKey(model, "tab")
model = updateRunes(model, "localhost:8888")
model = updateKey(model, "ctrl+s")
if calls != 0 {
t.Fatalf("ctrl+s triggered %d probe call(s), want 0", calls)
}
if len(module.intents) != 1 || module.intents[0].Kind != app.CreateProfile {
t.Fatalf("ctrl+s dispatched %#v", module.intents)
}
}
func TestProtocolProbeOnlyTriggersOnSingleURLField(t *testing.T) {
module := &recordingModule{}
model := NewModel(module)
calls := 0
model.detect = func(_ context.Context, address string) (probe.Capabilities, error) {
calls++
return probe.Capabilities{HTTP: true}, nil
}
model = updateKey(model, "m")
model = updateKey(model, "n")
model = updateRunes(model, "home")
// Field 0 is Name.
model, cmd := pressCtrlD(model)
if cmd != nil {
t.Fatal("ctrl+d on Name field started a Protocol Probe")
}
// Move to Single URL and probe once so we can reach the Bypass field.
model = updateKey(model, "tab")
model = updateRunes(model, "localhost:8888")
model, cmd = pressCtrlD(model)
if cmd == nil {
t.Fatal("ctrl+d on Single URL field did not start a Protocol Probe")
}
updated, _ := model.Update(cmd())
model = updated.(Model)
// Move to Bypass (field 2) and press Ctrl+D again.
model = updateKey(model, "tab")
model, cmd = pressCtrlD(model)
if cmd != nil {
t.Fatal("ctrl+d on Bypass field started a Protocol Probe")
}
if calls != 1 {
t.Fatalf("Protocol Probe ran %d times, want 1", calls)
}
}
func TestProtocolProbeDetectsDualProtocolEndpointBuildsCompatibleAdvancedProfile(t *testing.T) {
module := &recordingModule{}
model := NewModel(module)
model.detect = func(_ context.Context, address string) (probe.Capabilities, error) {
return probe.Capabilities{HTTP: true, SOCKS5: true}, nil
}
model = updateKey(model, "m")
model = updateKey(model, "n")
model = updateRunes(model, "home")
model = updateKey(model, "tab")
model = updateRunes(model, "localhost:7890")
model, cmd := pressCtrlD(model)
if cmd == nil {
t.Fatal("ctrl+d did not start a Protocol Probe")
}
updated, _ := model.Update(cmd())
model = updated.(Model)
got := model.form.profile
if got.Mode != profile.AdvancedMode || got.HTTPURL != "http://localhost:7890" || got.HTTPSURL != "http://localhost:7890" || got.AllURL != "socks5://localhost:7890" {
t.Fatalf("dual-protocol Profile = %#v", got)
}
if view := model.View(); !strings.Contains(view, "Protocol Probe: detected HTTP and SOCKS5 proxy at localhost:7890") {
t.Fatalf("form view missing dual-protocol detection status:\n%s", view)
}
}
func TestProtocolProbeDetectsSOCKS5AndRewritesSingleURL(t *testing.T) {
module := &recordingModule{}
model := NewModel(module)
model.detect = func(_ context.Context, address string) (probe.Capabilities, error) {
return probe.Capabilities{SOCKS5: true}, nil
}
model = updateKey(model, "m")
model = updateKey(model, "n")
model = updateRunes(model, "home")
model = updateKey(model, "tab")
model = updateRunes(model, "localhost:8888")
model, cmd := pressCtrlD(model)
if cmd == nil {
t.Fatal("ctrl+d did not start a Protocol Probe")
}
updated, _ := model.Update(cmd())
model = updated.(Model)
if got := model.form.profile.SingleURL; got != "socks5h://localhost:8888" {
t.Fatalf("Single URL after SOCKS5 probe = %q, want socks5h://localhost:8888", got)
}
if view := model.View(); !strings.Contains(view, "Protocol Probe: detected SOCKS5 proxy at localhost:8888") {
t.Fatalf("form view missing SOCKS5 detection status:\n%s", view)
}
}
func TestProtocolProbeInAdvancedModeShowsHintWithoutProbing(t *testing.T) {
module := &recordingModule{}
model := NewModel(module)
calls := 0
model.detect = func(_ context.Context, address string) (probe.Capabilities, error) {
calls++
return probe.Capabilities{HTTP: true}, nil
}
model = updateKey(model, "m")
model = updateKey(model, "n")
model = updateRunes(model, "home")
model = updateKey(model, "ctrl+t")
model = updateKey(model, "tab")
model = updateRunes(model, "localhost:8888")
model, cmd := pressCtrlD(model)
if cmd != nil {
t.Fatal("ctrl+d in Advanced mode started a Protocol Probe")
}
if calls != 0 {
t.Fatalf("Advanced mode probe ran %d times, want 0", calls)
}
if view := model.View(); strings.Contains(view, "Protocol Probe") || strings.Contains(view, "Ctrl+D") {
t.Fatalf("Advanced mode advertised an inapplicable Protocol Probe action:\n%s", view)
}
}
func updateKey(model Model, key string) Model {
msg := tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune(key)}
special := map[string]tea.KeyType{