package tui import ( "fmt" "strings" "testing" tea "github.com/charmbracelet/bubbletea" "proxyctl/internal/app" "proxyctl/internal/profile" ) func editorKey(model Model, key tea.KeyType) Model { updated, _ := model.Update(tea.KeyMsg{Type: key}) return updated.(Model) } func openNewProfileEditor(t *testing.T, module *profileManagementModule) Model { t.Helper() model := resizedModel(t, module, themeModeNoColor, 80, 24) model = updateKey(model, "m") return updateKey(model, "n") } func TestCompleteModelProfileEditorSupportsCursorEditingDeletionAndPaste(t *testing.T) { module := &profileManagementModule{} model := openNewProfileEditor(t, module) model = updateRunes(model, "ofice") model = editorKey(model, tea.KeyLeft) model = editorKey(model, tea.KeyLeft) model = editorKey(model, tea.KeyLeft) model = updateRunes(model, "f") model = editorKey(model, tea.KeyHome) model = updateRunes(model, "x") model = editorKey(model, tea.KeyBackspace) model = editorKey(model, tea.KeyEnd) model = editorKey(model, tea.KeyBackspace) model = updateRunes(model, "e") model = updateKey(model, "tab") model = updateRunes(model, "http://proxy.example:808x0") // multi-rune KeyMsg is Bubble Tea's paste boundary model = editorKey(model, tea.KeyLeft) model = editorKey(model, tea.KeyLeft) model = editorKey(model, tea.KeyDelete) model = updateKey(model, "ctrl+s") if len(module.intents) != 1 { t.Fatalf("save dispatched %d intents, want 1; frame:\n%s", len(module.intents), model.View()) } got := module.intents[0].Profile if got.Name != "office" || got.SingleURL != "http://proxy.example:8080" { t.Fatalf("edited Profile = %#v", got) } } func TestCompleteModelProfileEditorHorizontallyScrollsLongSecretURLWithoutDisclosure(t *testing.T) { module := &profileManagementModule{} model := openNewProfileEditor(t, module) model = updateRunes(model, "office") model = updateKey(model, "tab") const secret = "credential-never-rendered" const raw = "https://operator:" + secret + "@this-is-a-very-long-proxy-hostname-for-horizontal-scroll.example:8443" model = updateRunes(model, raw) end := model.View() assertUsableFrame(t, end, 80, 24, "[focused]", "scroll.example:8443") if strings.Contains(end, secret) || strings.Contains(end, "https://operator") { t.Fatalf("end-scrolled input disclosed credentials or failed to scroll:\n%s", end) } model = editorKey(model, tea.KeyHome) start := model.View() assertUsableFrame(t, start, 80, 24, "https://operator:") if strings.Contains(start, secret) || strings.Contains(start, "scroll.example:8443") { t.Fatalf("home did not scroll to the safe beginning of the input:\n%s", start) } } func TestCompleteModelProfileEditorPreservesValuesAcrossSingleAdvancedTransitions(t *testing.T) { module := &profileManagementModule{} model := openNewProfileEditor(t, module) model = updateRunes(model, "office") model = updateKey(model, "tab") model = updateRunes(model, "proxy.example:8080") model = updateKey(model, "ctrl+t") model = updateKey(model, "tab") model = editorKey(model, tea.KeyHome) model = updateRunes(model, "http://") model = updateKey(model, "ctrl+t") model = updateKey(model, "ctrl+t") model = updateKey(model, "ctrl+s") if len(module.intents) != 1 { t.Fatalf("save dispatched %d intents; frame:\n%s", len(module.intents), model.View()) } got := module.intents[0].Profile if got.Mode != profile.AdvancedMode || got.HTTPURL != "http://proxy.example:8080" || got.HTTPSURL != "http://proxy.example:8080" || got.AllURL != "http://proxy.example:8080" { t.Fatalf("transition discarded values: %#v", got) } } func TestCompleteModelProfileEditorShowsCanonicalFieldValidationAndLiveBypassAnalysis(t *testing.T) { module := &profileManagementModule{} model := openNewProfileEditor(t, module) model = updateRunes(model, "office") model = updateKey(model, "tab") model = updateRunes(model, "proxy.example") view := model.View() assertUsableFrame(t, view, 80, 24, "Single proxy URL must include a port") model = editorKey(model, tea.KeyEnd) model = updateRunes(model, ":8080") if view := model.View(); strings.Contains(view, "must include a port") { t.Fatalf("correcting the URL left a stale field error:\n%s", view) } model = updateKey(model, "tab") model = updateRunes(model, "LOCALHOST, .Example.COM, localhost, bad entry, 10.0.0.7/24") view = model.View() assertUsableFrame(t, view, 80, 24, "Normalized Bypass preview", "localhost", ".example.com", "10.0.0.0/24", "bad entry", "invalid") if strings.Count(view, "• localhost") != 1 { t.Fatalf("Bypass preview did not deduplicate localhost:\n%s", view) } model = updateKey(model, "ctrl+s") if len(module.intents) != 0 { t.Fatalf("invalid Bypass dispatched %#v", module.intents) } model = editorKey(model, tea.KeyCtrlU) model = updateRunes(model, "LOCALHOST, .Example.COM, localhost, 10.0.0.7/24") if view := model.View(); strings.Contains(view, "bad entry") || strings.Contains(view, "invalid") { t.Fatalf("correcting Bypass left stale item validation:\n%s", view) } model = updateKey(model, "ctrl+s") if len(module.intents) != 1 { t.Fatalf("valid save dispatched %d intents; frame:\n%s", len(module.intents), model.View()) } want := []string{"localhost", ".example.com", "10.0.0.0/24"} got := module.intents[0].Profile.Bypass if strings.Join(got, "|") != strings.Join(want, "|") { t.Fatalf("saved Bypass = %#v, want %#v", got, want) } } func TestCompleteModelProfileEditorScrollsLongBypassPreviewAt80Columns(t *testing.T) { module := &profileManagementModule{} model := openNewProfileEditor(t, module) model = updateRunes(model, "office") model = updateKey(model, "tab") model = updateRunes(model, "proxy.example:8080") model = updateKey(model, "tab") items := make([]string, 0, 25) for i := range 24 { items = append(items, fmt.Sprintf("host-%02d.example", i)) } items = append(items, "late invalid entry") model = updateRunes(model, strings.Join(items, ",")) if strings.Contains(model.View(), "! item 25") { t.Fatal("fixture did not exceed the 80x24 preview viewport") } for range 6 { model = updateKey(model, "pgdown") } model = resize(model, 80, 24) assertUsableFrame(t, model.View(), 80, 24, "> Bypass [focused]", "! item 25", "late invalid entry", "invalid", "PgUp/PgDn: scroll form") model = updateKey(model, "pgup") view := model.View() assertUsableFrame(t, view, 80, 24, "> Bypass [focused]", "host-19.example", "PgUp/PgDn: scroll form") if strings.Contains(view, "! item 25") { t.Fatalf("PgUp did not move the persistent Bypass preview viewport:\n%s", view) } } func TestCompleteModelProfileEditorObscuresCredentialsInScrolledBypassValidation(t *testing.T) { module := &profileManagementModule{} model := openNewProfileEditor(t, module) model = updateRunes(model, "office") model = updateKey(model, "tab") model = updateRunes(model, "proxy.example:8080") model = updateKey(model, "tab") items := make([]string, 0, 25) for i := range 24 { items = append(items, fmt.Sprintf("host-%02d.example", i)) } const secret = "late-bypass-secret" items = append(items, "http://operator:"+secret+"@proxy.invalid") model = updateRunes(model, strings.Join(items, ",")) for range 6 { model = updateKey(model, "pgdown") } for _, size := range []struct{ width, height int }{{80, 24}, {60, 16}, {120, 36}, {80, 24}} { model = resize(model, size.width, size.height) view := model.View() assertUsableFrame(t, view, size.width, size.height, "> Bypass [focused]", "REDACTED@proxy.invalid") if strings.Contains(view, secret) { t.Fatalf("scrolled Bypass validation exposed credentials at %dx%d:\n%s", size.width, size.height, view) } } } func TestCompleteModelProfileEditorObscuresSchemeLessCredentialsDuringEntryValidationAndScroll(t *testing.T) { module := &profileManagementModule{} model := openNewProfileEditor(t, module) model = updateRunes(model, "office") model = updateKey(model, "tab") const secret = "scheme-less-secret" const raw = "operator:" + secret + "@this-is-a-very-long-proxy-hostname-for-scroll.example:notaport" model = updateRunes(model, raw) for _, step := range []func(Model) Model{ func(value Model) Model { return value }, func(value Model) Model { return editorKey(value, tea.KeyHome) }, func(value Model) Model { return editorKey(value, tea.KeyEnd) }, func(value Model) Model { return updateKey(value, "ctrl+s") }, func(value Model) Model { return resize(value, 60, 16) }, func(value Model) Model { return resize(value, 80, 24) }, } { model = step(model) if view := model.View(); strings.Contains(view, secret) { t.Fatalf("scheme-less credential rendered during entry, validation, or scroll:\n%s", view) } } if len(module.intents) != 0 { t.Fatalf("invalid credential-bearing URL dispatched %#v", module.intents) } } func TestCompleteModelProfileEditorShowsNameConflictsInlineAndBlocksContradictorySave(t *testing.T) { module := &profileManagementModule{snapshot: app.Snapshot{Profiles: []profile.Profile{{ Name: "office", Mode: profile.SingleMode, SingleURL: "http://proxy.example:8080", }}}} model := openNewProfileEditor(t, module) model = updateRunes(model, "office") model = updateKey(model, "tab") model = updateRunes(model, "other.example:8080") assertUsableFrame(t, model.View(), 80, 24, "Name", "a Profile named \"office\" already exists") model = updateKey(model, "ctrl+s") if len(module.intents) != 0 { t.Fatalf("conflicting name dispatched %#v", module.intents) } } func TestCompleteModelProfileEditorSaveSelectsSavedProfileAndCancelDoesNotPersist(t *testing.T) { module := &profileManagementModule{snapshot: app.Snapshot{Profiles: []profile.Profile{{ Name: "existing", Mode: profile.SingleMode, SingleURL: "http://existing.example:8080", }}}} model := openNewProfileEditor(t, module) model = updateRunes(model, "discarded") model = updateKey(model, "tab") model = updateRunes(model, "http://discarded.example:8080") model = updateKey(model, "esc") if len(module.intents) != 0 || len(module.snapshot.Profiles) != 1 { t.Fatalf("cancel mutated persistence: intents=%#v snapshot=%#v", module.intents, module.snapshot) } model = updateKey(model, "n") model = updateRunes(model, "saved") model = updateKey(model, "tab") model = updateRunes(model, "saved.example:8080") model = updateKey(model, "ctrl+s") assertUsableFrame(t, model.View(), 80, 24, "> saved", "Proxy: http://saved.example:8080") if len(module.intents) != 1 || module.intents[0].Kind != app.CreateProfile { t.Fatalf("save dispatched %#v", module.intents) } } func TestCompleteModelProfileEditorResizeAndErrorsNeverRenderAdversarialCredentials(t *testing.T) { module := &profileManagementModule{} model := openNewProfileEditor(t, module) model = updateRunes(model, "office") model = updateKey(model, "tab") const secret = "adversarial-secret" model = updateRunes(model, "http://alice:"+secret+"@proxy.example:notaport") model = updateKey(model, "tab") model = updateRunes(model, "http://bob:"+secret+"@bypass.invalid") for _, size := range []struct{ width, height int }{{80, 24}, {120, 36}, {60, 16}, {80, 24}} { model = resize(model, size.width, size.height) view := model.View() assertUsableFrame(t, view, size.width, size.height, "Ctrl+S: save", "Esc: cancel") if strings.Contains(view, secret) { t.Fatalf("secret rendered after resize to %dx%d:\n%s", size.width, size.height, view) } } }