feat: add persistent proxy profile management
This commit is contained in:
@@ -0,0 +1,118 @@
|
||||
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 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}
|
||||
}
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user