package tui import ( "fmt" "slices" "strings" "testing" tea "github.com/charmbracelet/bubbletea" "proxyctl/internal/app" "proxyctl/internal/profile" ) // profileManagementModule is an in-memory Application seam used to drive the // same complete Model as production while making CRUD results observable. type profileManagementModule struct { snapshot app.Snapshot intents []app.Intent reverseUpdate bool } func (m *profileManagementModule) Snapshot() app.Snapshot { return m.snapshot } func (m *profileManagementModule) Dispatch(intent app.Intent) app.Result { m.intents = append(m.intents, intent) switch intent.Kind { case app.CreateProfile: m.snapshot.Profiles = append(m.snapshot.Profiles, intent.Profile) case app.UpdateProfile: for i := range m.snapshot.Profiles { if m.snapshot.Profiles[i].Name == intent.Name { m.snapshot.Profiles[i] = intent.Profile break } } if m.snapshot.ActiveProfile == intent.Name { m.snapshot.ActiveProfile = intent.Profile.Name } if m.reverseUpdate { slices.Reverse(m.snapshot.Profiles) } case app.DeleteProfile: for i := range m.snapshot.Profiles { if m.snapshot.Profiles[i].Name == intent.Name { m.snapshot.Profiles = append(m.snapshot.Profiles[:i], m.snapshot.Profiles[i+1:]...) break } } if m.snapshot.ActiveProfile == intent.Name { m.snapshot.ActiveProfile = "" } } return app.Result{Snapshot: m.snapshot, Notice: "done"} } func (m *profileManagementModule) StartOperation(intent app.Intent) app.OperationStream { result := m.Dispatch(intent) updates := make(chan app.OperationUpdate, 1) updates <- app.SucceededOperationUpdate(result.Snapshot, app.OperationStatus{Summary: result.Notice}, nil) close(updates) return app.OperationStream{Snapshot: result.Snapshot, Updates: updates} } func TestCompleteModelProfileManagementScrollsAndReviewsObscuredDetailsAcrossResize(t *testing.T) { profiles := make([]profile.Profile, 30) for i := range profiles { profiles[i] = profile.Profile{ Name: fmt.Sprintf("site-%02d", i), Mode: profile.SingleMode, SingleURL: fmt.Sprintf("https://operator:secret-%02d@proxy-%02d.example:8443", i, i), Bypass: []string{fmt.Sprintf("internal-%02d.example", i), "localhost"}, } } module := &profileManagementModule{snapshot: app.Snapshot{ActiveProfile: "site-00", Profiles: profiles}} model := resizedModel(t, module, themeModeNoColor, 80, 24) model = updateKey(model, "m") for range 24 { model = updateKey(model, "down") } 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, "> site-24", "Mode: single", "https://operator:REDACTED@proxy-24.example:8443", "internal-24.example") if strings.Contains(view, "secret-24") { t.Fatalf("Profile details exposed credentials after resize to %dx%d:\n%s", size.width, size.height, view) } if size.width == 60 && size.height == 16 { model = updateKey(model, "pgdown") assertUsableFrame(t, model.View(), 60, 16, "localhost") model = updateKey(model, "pgup") } } } func TestCompleteModelProfileManagementHasCreateAndEditEntryPointsButNoActivationAction(t *testing.T) { module := &profileManagementModule{snapshot: app.Snapshot{Profiles: []profile.Profile{{ Name: "office", Mode: profile.SingleMode, SingleURL: "http://proxy.example:8080", }}}} model := resizedModel(t, module, themeModeNoColor, 80, 24) model = updateKey(model, "m") view := model.View() assertUsableFrame(t, view, 80, 24, "n: create", "e: edit", "d: delete", "q/Esc: back") if strings.Contains(view, "activate") { t.Fatalf("management page mixed in Active Profile switching:\n%s", view) } model = updateKey(model, "n") if view := model.View(); !strings.Contains(view, "Create Proxy Profile") { t.Fatalf("create entry point did not open the existing editor:\n%s", view) } model = updateKey(model, "esc") model = updateKey(model, "e") if view := model.View(); !strings.Contains(view, "Edit Proxy Profile") || !strings.Contains(view, "office") { t.Fatalf("edit entry point did not open the existing editor:\n%s", view) } } func TestCompleteModelProfileDeletionDialogIsSafeByDefaultAndSupportsExplicitSelection(t *testing.T) { module := &profileManagementModule{snapshot: app.Snapshot{Profiles: []profile.Profile{{ Name: "office", Mode: profile.SingleMode, SingleURL: "http://proxy.example:8080", }, { Name: "remote", Mode: profile.SingleMode, SingleURL: "http://remote.example:8080", }}}} model := resizedModel(t, module, themeModeNoColor, 80, 24) model = updateKey(model, "m") model = updateKey(model, "d") view := model.View() assertUsableFrame(t, view, 80, 24, "Delete Proxy Profile “office”?", "> Cancel", "Delete", "Enter: choose") updated, command := model.Update(tea.KeyMsg{Type: tea.KeyEnter}) model = updated.(Model) if command != nil || len(module.intents) != 0 || strings.Contains(model.View(), "Delete Proxy Profile “office”?") { t.Fatalf("safe-default Enter did not cancel deletion: intents=%#v\n%s", module.intents, model.View()) } model = updateKey(model, "d") model = updateKey(model, "q") if len(module.intents) != 0 || strings.Contains(model.View(), "Delete Proxy Profile “office”?") { t.Fatalf("q did not cancel deletion without mutation: intents=%#v\n%s", module.intents, model.View()) } model = updateKey(model, "d") model = updateKey(model, "right") updated, command = model.Update(tea.KeyMsg{Type: tea.KeyEnter}) model = updated.(Model) if command != nil || len(module.intents) != 1 || module.intents[0].Kind != app.DeleteProfile || module.intents[0].Name != "office" || !module.intents[0].Confirm { t.Fatalf("focused Delete did not dispatch the confirmed mutation: intents=%#v", module.intents) } assertUsableFrame(t, model.View(), 80, 24, "> remote", "Proxy: http://remote.example:8080") } func TestCompleteModelFinalProfileDeletionExplainsDriftAndRequiresExplicitConfirmation(t *testing.T) { module := &profileManagementModule{snapshot: app.Snapshot{ActiveProfile: "office", Profiles: []profile.Profile{{ Name: "office", Mode: profile.SingleMode, SingleURL: "http://proxy.example:8080", }}}} model := resizedModel(t, module, themeModeNoColor, 80, 24) model = updateKey(model, "m") model = updateKey(model, "d") model = resize(model, 60, 16) view := model.View() assertUsableFrame(t, view, 60, 16, "final Proxy Profile", "managed configuration remains", "become Drifted", "> Cancel") model = updateKey(model, "y") if len(module.intents) != 1 || module.intents[0].Kind != app.DeleteProfile || module.intents[0].Name != "office" || !module.intents[0].Confirm { t.Fatalf("explicit confirmation dispatched %#v", module.intents) } model = resize(model, 80, 24) view = model.View() assertUsableFrame(t, view, 80, 24, "No Proxy Profiles saved.", "Press n to create one.") if strings.Contains(view, "No Active Profile") { t.Fatalf("management empty state conflated saved Profiles with Active Profile state:\n%s", view) } } func TestCompleteModelProfileSelectionUsesIdentityAcrossResizeAndReorderedCRUDResult(t *testing.T) { profiles := make([]profile.Profile, 22) for i := range profiles { profiles[i] = profile.Profile{Name: fmt.Sprintf("profile-%02d", i), Mode: profile.SingleMode, SingleURL: fmt.Sprintf("http://proxy-%02d.example:8080", i)} } module := &profileManagementModule{snapshot: app.Snapshot{Profiles: profiles}, reverseUpdate: true} model := resizedModel(t, module, themeModeNoColor, 80, 24) model = updateKey(model, "m") for range 17 { model = updateKey(model, "down") } model = resize(model, 120, 36) model = resize(model, 60, 16) model = updateKey(model, "e") model = updateKey(model, "ctrl+s") view := model.View() assertUsableFrame(t, view, 60, 16, "> profile-17", "Proxy: http://proxy-17.example:8080") if len(module.intents) != 1 || module.intents[0].Kind != app.UpdateProfile || module.intents[0].Name != "profile-17" { t.Fatalf("edit dispatched %#v", module.intents) } } func TestCompleteModelProfileManagementDoesNotConflateSavedAndActiveProfileState(t *testing.T) { module := &profileManagementModule{snapshot: app.Snapshot{Profiles: []profile.Profile{{ Name: "available", Mode: profile.SingleMode, SingleURL: "http://proxy.example:8080", }}}} model := resizedModel(t, module, themeModeNoColor, 80, 24) model = updateKey(model, "m") view := model.View() assertUsableFrame(t, view, 80, 24, "> available", "Selected Profile") if strings.Contains(view, "No Active Profile") || strings.Contains(view, "No Proxy Profiles") { t.Fatalf("management page misreported a non-active saved Profile as empty:\n%s", view) } } func TestCompleteModelProfileManagementNavigationAndBusyReasonsAreConsistent(t *testing.T) { module := &profileManagementModule{snapshot: app.Snapshot{Profiles: []profile.Profile{{Name: "office"}}}} for _, back := range []tea.KeyMsg{{Type: tea.KeyEsc}, {Type: tea.KeyRunes, Runes: []rune{'q'}}} { model := resizedModel(t, module, themeModeNoColor, 80, 24) model = updateKey(model, "m") updated, command := model.Update(back) model = updated.(Model) if command != nil || !strings.Contains(model.View(), "Operations Console") { t.Fatalf("%q did not return to the dashboard:\n%s", back.String(), model.View()) } } model := resizedModel(t, module, themeModeNoColor, 80, 24) model = updateKey(model, "m") updated, command := model.Update(tea.KeyMsg{Type: tea.KeyCtrlC}) if command == nil { t.Fatal("Ctrl+C on Profile management did not follow the global exit contract") } if _, ok := command().(tea.QuitMsg); !ok { t.Fatal("Ctrl+C on Profile management did not emit tea.Quit") } _ = updated checking := &profileManagementModule{snapshot: app.Snapshot{ Profiles: []profile.Profile{{Name: "office"}}, Targets: []app.TargetSnapshot{{ID: "shell", DetectionPhase: app.TargetChecking}}, }} busy := resizedModel(t, checking, themeModeNoColor, 80, 24) busy = updateKey(busy, "m") if view := busy.View(); !strings.Contains(view, "Profile management disabled: wait for the current mutation to finish") { t.Fatalf("disabled Profile management action did not expose its reason:\n%s", view) } }