sshkeeper/internal/tui/form_validation_test.go

196 lines
6.1 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 view := fm.View(); !strings.Contains(view, "Port must be a number") {
t.Fatalf("validation error is not rendered:\n%s", view)
}
}
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)
}
}
}