package tui import ( "strings" "testing" tea "github.com/charmbracelet/bubbletea" "proxyctl/internal/app" "proxyctl/internal/profile" ) type recordingModule struct { snapshot app.Snapshot intents []app.Intent } func (m *recordingModule) Snapshot() app.Snapshot { return m.snapshot } func (m *recordingModule) Dispatch(intent app.Intent) app.Result { m.intents = append(m.intents, intent) return app.Result{Snapshot: m.snapshot, Notice: "done"} } func TestMainAndProfilePagesMapKeysAndObscurePasswords(t *testing.T) { module := &recordingModule{snapshot: app.Snapshot{ ActiveProfile: "office", Profiles: []profile.Profile{{ Name: "office", Mode: profile.SingleMode, SingleURL: "https://alice:very-secret@proxy.example:8443", }}, }} model := NewModel(module) if view := model.View(); !strings.Contains(view, "Active Profile: office") { t.Fatalf("main view missing Active Profile:\n%s", view) } model = updateKey(model, "p") view := model.View() if !strings.Contains(view, "Proxy Profiles") || !strings.Contains(view, "alice:") { t.Fatalf("Profile page did not render selected Profile:\n%s", view) } if strings.Contains(view, "very-secret") { t.Fatalf("Profile page exposed proxy password:\n%s", view) } model = updateKey(model, "a") if len(module.intents) != 1 || module.intents[0].Kind != app.ActivateProfile || module.intents[0].Name != "office" { t.Fatalf("activate key dispatched %#v", module.intents) } model = updateKey(model, "d") if !strings.Contains(model.View(), "Delete Profile") { t.Fatalf("delete key did not request confirmation:\n%s", model.View()) } model = updateKey(model, "y") if len(module.intents) != 2 || module.intents[1].Kind != app.DeleteProfile || !module.intents[1].Confirm { t.Fatalf("confirmed delete dispatched %#v", module.intents) } } func TestProfileFormMapsKeyboardInputToCreateIntent(t *testing.T) { module := &recordingModule{} model := NewModel(module) model = updateKey(model, "p") model = updateKey(model, "n") model = updateRunes(model, "home") model = updateKey(model, "tab") model = updateRunes(model, "socks5://localhost:1080") model = updateKey(model, "tab") model = updateRunes(model, "localhost, .example.com") model = updateKey(model, "ctrl+s") if len(module.intents) != 1 { t.Fatalf("create form dispatched %d intents", len(module.intents)) } intent := module.intents[0] if intent.Kind != app.CreateProfile || intent.Profile.Name != "home" || intent.Profile.SingleURL != "socks5://localhost:1080" { t.Fatalf("create form dispatched %#v", intent) } if got := intent.Profile.Bypass; len(got) != 1 || got[0] != "localhost, .example.com" { t.Fatalf("create form Bypass = %#v", got) } } func TestEditingSingleURLExpandsCurrentValueWhenSwitchingToAdvanced(t *testing.T) { const oldURL = "http://old.example:8080" const newURL = "socks5h://new.example:1080" module := &recordingModule{snapshot: app.Snapshot{Profiles: []profile.Profile{{ Name: "office", Mode: profile.SingleMode, SingleURL: oldURL, HTTPURL: oldURL, HTTPSURL: oldURL, AllURL: oldURL, }}}} model := NewModel(module) model = updateKey(model, "p") model = updateKey(model, "e") model = updateKey(model, "tab") for range oldURL { model = updateKey(model, "backspace") } model = updateRunes(model, newURL) model = updateKey(model, "ctrl+t") model = updateKey(model, "ctrl+s") if len(module.intents) != 1 { t.Fatalf("edit form dispatched %d intents", len(module.intents)) } got := module.intents[0].Profile if got.HTTPURL != newURL || got.HTTPSURL != newURL || got.AllURL != newURL { t.Fatalf("advanced URLs = %q, %q, %q; want current Single URL %q", got.HTTPURL, got.HTTPSURL, got.AllURL, newURL) } } func TestRenderedMalformedProxyURLStillObscuresPassword(t *testing.T) { module := &recordingModule{snapshot: app.Snapshot{Profiles: []profile.Profile{{ Name: "manual", Mode: profile.SingleMode, SingleURL: "http://alice:do-not-show@proxy.example:bad", }}}} model := NewModel(module) model = updateKey(model, "p") if view := model.View(); strings.Contains(view, "do-not-show") { t.Fatalf("malformed proxy URL exposed password:\n%s", view) } } func TestNoProfileStateStaysOnAccessibleMainPage(t *testing.T) { model := NewModel(&recordingModule{}) view := model.View() if !strings.Contains(view, "No Active Profile") || !strings.Contains(view, "p: manage Proxy Profiles") { t.Fatalf("no-Profile main view is not actionable:\n%s", view) } } func updateKey(model Model, key string) Model { msg := tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune(key)} switch key { case "tab": msg = tea.KeyMsg{Type: tea.KeyTab} case "ctrl+s": msg = tea.KeyMsg{Type: tea.KeyCtrlS} case "ctrl+t": msg = tea.KeyMsg{Type: tea.KeyCtrlT} case "backspace": msg = tea.KeyMsg{Type: tea.KeyBackspace} } updated, _ := model.Update(msg) return updated.(Model) } func updateRunes(model Model, value string) Model { updated, _ := model.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune(value)}) return updated.(Model) }