276 lines
8.7 KiB
Go
276 lines
8.7 KiB
Go
package tui
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
|
|
tea "github.com/charmbracelet/bubbletea"
|
|
"github.com/mirivlad/sshkeeper/internal/model"
|
|
)
|
|
|
|
func TestParsePortRejectsInvalidValues(t *testing.T) {
|
|
for _, value := range []string{"", "abc", "0", "65536", "22x"} {
|
|
t.Run(value, func(t *testing.T) {
|
|
if _, err := parsePort(value); err == nil {
|
|
t.Fatalf("parsePort(%q) succeeded", value)
|
|
}
|
|
})
|
|
}
|
|
if port, err := parsePort("22"); err != nil || port != 22 {
|
|
t.Fatalf("parsePort(22) = %d, %v", port, err)
|
|
}
|
|
}
|
|
|
|
func TestServerFormPreservesInvalidPortAndDoesNotSave(t *testing.T) {
|
|
fm := newFormModel(80, 24)
|
|
fm.inputs[0].SetValue("prod")
|
|
fm.inputs[2].SetValue("prod.example")
|
|
fm.inputs[3].SetValue("abc")
|
|
oldSave := SaveServer
|
|
t.Cleanup(func() { SaveServer = oldSave })
|
|
saves := 0
|
|
SaveServer = func(*model.Server, string, string) error {
|
|
saves++
|
|
return nil
|
|
}
|
|
|
|
cmd := fm.runSave()
|
|
if cmd == nil {
|
|
t.Fatal("expected validation result command")
|
|
}
|
|
updated, _ := fm.Update(cmd())
|
|
fm = updated.(*formModel)
|
|
if saves != 0 || fm.inputs[3].Value() != "abc" {
|
|
t.Fatalf("invalid input was lost or saved: saves=%d value=%q", saves, fm.inputs[3].Value())
|
|
}
|
|
if fm.err == nil || !strings.Contains(fm.err.Error(), "Port") {
|
|
t.Fatalf("missing actionable port error: %v", fm.err)
|
|
}
|
|
if fm.focusIdx != 3 {
|
|
t.Fatalf("invalid port focus = %d, want 3", fm.focusIdx)
|
|
}
|
|
if view := fm.View(); !strings.Contains(view, "Port must be a number") {
|
|
t.Fatalf("validation error is not rendered:\n%s", view)
|
|
}
|
|
}
|
|
|
|
func TestServerSaveSuccessResetsDirtySnapshot(t *testing.T) {
|
|
m := New(nil)
|
|
m.screen = screenForm
|
|
m.form = newFormModel(80, 24)
|
|
m.form.inputs[0].SetValue("prod")
|
|
m.form.inputs[2].SetValue("prod.example")
|
|
m.form.password.SetValue("secret")
|
|
|
|
updated, _ := m.Update(saveDoneMsg{})
|
|
m = updated.(*tuiModel)
|
|
if m.form == nil || m.form.Dirty() {
|
|
t.Fatalf("saved form remains dirty: %#v", m.form)
|
|
}
|
|
if m.form.password.Value() != "" {
|
|
t.Fatal("saved secret remained in the form")
|
|
}
|
|
}
|
|
|
|
func TestForwardValidationMovesFocusToInvalidPort(t *testing.T) {
|
|
fm := newForwardFormModel(1, 60, 16)
|
|
fm.nameInput.SetValue("postgres")
|
|
fm.inputs[0].SetValue("127.0.0.1")
|
|
fm.inputs[1].SetValue("bad")
|
|
fm.inputs[2].SetValue("db")
|
|
fm.inputs[3].SetValue("5432")
|
|
updated, _ := fm.Update(fm.runSave()())
|
|
fm = updated.(*forwardFormModel)
|
|
if fm.focusIdx != 6 {
|
|
t.Fatalf("invalid listen port focus = %d, want 6", fm.focusIdx)
|
|
}
|
|
view := fm.View()
|
|
if !strings.Contains(view, "Listen Port") || !strings.Contains(view, "must be a number") {
|
|
t.Fatalf("invalid port and error are not visible together:\n%s", view)
|
|
}
|
|
}
|
|
|
|
func TestCtrlQQuitsCleanStateAndConfirmsDirtyForm(t *testing.T) {
|
|
t.Run("clean manager", func(t *testing.T) {
|
|
m := New(nil)
|
|
m.screen = screenForwardList
|
|
m.forwardScreen = newForwardScreenModel(1, "prod", 80, 24)
|
|
_, cmd := m.Update(tea.KeyMsg{Type: tea.KeyCtrlQ})
|
|
if cmd == nil {
|
|
t.Fatal("Ctrl+Q did not quit clean manager")
|
|
}
|
|
if _, ok := cmd().(tea.QuitMsg); !ok {
|
|
t.Fatalf("Ctrl+Q command returned %T", cmd())
|
|
}
|
|
})
|
|
|
|
t.Run("dirty server form", func(t *testing.T) {
|
|
m := New(nil)
|
|
m.screen = screenForm
|
|
m.form = newFormModel(80, 24)
|
|
m.form.inputs[0].SetValue("prod")
|
|
updated, cmd := m.Update(tea.KeyMsg{Type: tea.KeyCtrlQ})
|
|
m = updated.(*tuiModel)
|
|
if cmd != nil || m.screen != screenConfirm || m.confirm == nil || m.confirm.focus != confirmCancel {
|
|
t.Fatalf("dirty Ctrl+Q did not open safe discard: screen=%v confirm=%#v", m.screen, m.confirm)
|
|
}
|
|
m.confirm.focus = confirmAccept
|
|
updated, cmd = m.updateConfirm(tea.KeyMsg{Type: tea.KeyEnter})
|
|
m = updated.(*tuiModel)
|
|
if cmd == nil {
|
|
t.Fatal("accepted quit confirmation produced no command")
|
|
}
|
|
updated, quitCmd := m.Update(cmd())
|
|
m = updated.(*tuiModel)
|
|
if quitCmd == nil || m.confirm != nil || m.form != nil {
|
|
t.Fatalf("accepted dirty quit did not clear state: confirm=%v form=%v", m.confirm, m.form)
|
|
}
|
|
if _, ok := quitCmd().(tea.QuitMsg); !ok {
|
|
t.Fatalf("accepted dirty quit returned %T", quitCmd())
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestDirtyServerFormRequiresDiscardConfirmation(t *testing.T) {
|
|
oldList := ListServers
|
|
t.Cleanup(func() { ListServers = oldList })
|
|
ListServers = func() ([]*model.Server, error) { return nil, nil }
|
|
|
|
m := New(nil)
|
|
m.screen = screenForm
|
|
m.form = newFormModel(80, 24)
|
|
m.form.inputs[0].SetValue("prod")
|
|
|
|
updated, cmd := m.updateForm(tea.KeyMsg{Type: tea.KeyEsc})
|
|
m = updated.(*tuiModel)
|
|
if cmd != nil || m.screen != screenConfirm || m.confirm == nil || m.confirm.parent != screenForm {
|
|
t.Fatalf("dirty form did not open discard confirmation: screen=%v confirm=%#v", m.screen, m.confirm)
|
|
}
|
|
updated, _ = m.updateConfirm(tea.KeyMsg{Type: tea.KeyEnter})
|
|
m = updated.(*tuiModel)
|
|
if m.screen != screenForm || m.form == nil || m.form.inputs[0].Value() != "prod" {
|
|
t.Fatalf("Cancel did not preserve form: screen=%v form=%#v", m.screen, m.form)
|
|
}
|
|
|
|
updated, _ = m.updateForm(tea.KeyMsg{Type: tea.KeyEsc})
|
|
m = updated.(*tuiModel)
|
|
updated, _ = m.updateConfirm(tea.KeyMsg{Type: tea.KeyTab})
|
|
m = updated.(*tuiModel)
|
|
updated, cmd = m.updateConfirm(tea.KeyMsg{Type: tea.KeyEnter})
|
|
m = updated.(*tuiModel)
|
|
if cmd == nil {
|
|
t.Fatal("expected discard command")
|
|
}
|
|
updated, _ = m.Update(cmd())
|
|
m = updated.(*tuiModel)
|
|
if m.screen != screenList || m.form != nil || m.confirm != nil {
|
|
t.Fatalf("discard did not return to list: screen=%v form=%v confirm=%v", m.screen, m.form, m.confirm)
|
|
}
|
|
}
|
|
|
|
func TestCleanFormsExitWithoutConfirmation(t *testing.T) {
|
|
oldList := ListServers
|
|
t.Cleanup(func() { ListServers = oldList })
|
|
ListServers = func() ([]*model.Server, error) { return nil, nil }
|
|
|
|
tests := []struct {
|
|
name string
|
|
screen screen
|
|
setup func(*tuiModel)
|
|
exit func(*tuiModel) (tea.Model, tea.Cmd)
|
|
want screen
|
|
}{
|
|
{
|
|
name: "server",
|
|
screen: screenForm,
|
|
setup: func(m *tuiModel) { m.form = newFormModel(80, 24) },
|
|
exit: func(m *tuiModel) (tea.Model, tea.Cmd) { return m.updateForm(tea.KeyMsg{Type: tea.KeyEsc}) },
|
|
want: screenList,
|
|
},
|
|
{
|
|
name: "forward",
|
|
screen: screenForwardForm,
|
|
setup: func(m *tuiModel) {
|
|
m.forwardScreen = newForwardScreenModel(1, "prod", 80, 24)
|
|
m.forwardForm = newForwardFormModel(1, 80, 24)
|
|
},
|
|
exit: func(m *tuiModel) (tea.Model, tea.Cmd) {
|
|
return m.updateForwardForm(tea.KeyMsg{Type: tea.KeyEsc})
|
|
},
|
|
want: screenForwardList,
|
|
},
|
|
{
|
|
name: "template",
|
|
screen: screenTemplateForm,
|
|
setup: func(m *tuiModel) { m.templateForm = newTemplateFormModel(nil, 80, 24) },
|
|
exit: func(m *tuiModel) (tea.Model, tea.Cmd) {
|
|
return m.updateTemplateForm(tea.KeyMsg{Type: tea.KeyEsc})
|
|
},
|
|
want: screenTemplates,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
m := New(nil)
|
|
m.screen = tt.screen
|
|
tt.setup(m)
|
|
updated, _ := tt.exit(m)
|
|
m = updated.(*tuiModel)
|
|
if m.screen != tt.want || m.confirm != nil {
|
|
t.Fatalf("clean exit: screen=%v confirm=%v", m.screen, m.confirm)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestDirtyForwardAndTemplateFormsRequireConfirmation(t *testing.T) {
|
|
t.Run("forward", func(t *testing.T) {
|
|
m := New(nil)
|
|
m.screen = screenForwardForm
|
|
m.forwardScreen = newForwardScreenModel(1, "prod", 80, 24)
|
|
m.forwardForm = newForwardFormModel(1, 80, 24)
|
|
m.forwardForm.nameInput.SetValue("postgres")
|
|
updated, _ := m.updateForwardForm(tea.KeyMsg{Type: tea.KeyEsc})
|
|
m = updated.(*tuiModel)
|
|
if m.screen != screenConfirm || m.confirm == nil || m.confirm.parent != screenForwardForm {
|
|
t.Fatalf("dirty forward form did not confirm: screen=%v confirm=%#v", m.screen, m.confirm)
|
|
}
|
|
})
|
|
|
|
t.Run("template", func(t *testing.T) {
|
|
m := New(nil)
|
|
m.screen = screenTemplateForm
|
|
m.templateForm = newTemplateFormModel(nil, 80, 24)
|
|
m.templateForm.inputs[0].SetValue("uptime")
|
|
updated, _ := m.updateTemplateForm(tea.KeyMsg{Type: tea.KeyEsc})
|
|
m = updated.(*tuiModel)
|
|
if m.screen != screenConfirm || m.confirm == nil || m.confirm.parent != screenTemplateForm {
|
|
t.Fatalf("dirty template form did not confirm: screen=%v confirm=%#v", m.screen, m.confirm)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestRequiredFieldsAreMarked(t *testing.T) {
|
|
serverView := newFormModel(100, 30).View()
|
|
for _, want := range []string{"Alias *", "Host *", "Port *"} {
|
|
if !strings.Contains(serverView, want) {
|
|
t.Fatalf("server form missing %q:\n%s", want, serverView)
|
|
}
|
|
}
|
|
|
|
forwardView := newForwardFormModel(1, 100, 30).View()
|
|
for _, want := range []string{"Name *", "Listen Port *", "Target Host *", "Target Port *"} {
|
|
if !strings.Contains(forwardView, want) {
|
|
t.Fatalf("forward form missing %q:\n%s", want, forwardView)
|
|
}
|
|
}
|
|
|
|
templateView := newTemplateFormModel(nil, 100, 30).View()
|
|
for _, want := range []string{"Name *", "Command *"} {
|
|
if !strings.Contains(templateView, want) {
|
|
t.Fatalf("template form missing %q:\n%s", want, templateView)
|
|
}
|
|
}
|
|
}
|