Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
122 changes: 69 additions & 53 deletions internal/app/screen_reachability.go
Original file line number Diff line number Diff line change
Expand Up @@ -419,69 +419,85 @@ func (rm *reachabilityModel) updateConfig(m *Model, msg tea.KeyMsg) (tea.Model,
case "esc":
m.screen = screenReachabilityDestinationList
case "up", "k":
maxField := 1
if rm.destination != nil && rm.destination.ManualIP {
maxField = 2
}
rm.configField = previousListIndex(rm.configField, maxField+1)
rm.configField = previousListIndex(rm.configField, rm.configMaxField()+1)
case "down", "j", "tab":
maxField := 1
if rm.destination != nil && rm.destination.ManualIP {
maxField = 2
}
rm.configField = nextListIndex(rm.configField, maxField+1)
rm.configField = nextListIndex(rm.configField, rm.configMaxField()+1)
case "left", "h":
if rm.configField == 0 && rm.protocolIdx > 0 {
rm.protocolIdx--
}
rm.adjustConfigProtocol(-1)
case "right", "l":
if rm.configField == 0 && rm.protocolIdx < len(reachabilityProtocols)-1 {
rm.protocolIdx++
}
rm.adjustConfigProtocol(1)
case "backspace":
switch rm.configField {
case 1:
if len(rm.portInput) > 0 {
rm.portInput = rm.portInput[:len(rm.portInput)-1]
}
case 2:
if len(rm.destinationIP) > 0 {
rm.destinationIP = rm.destinationIP[:len(rm.destinationIP)-1]
}
}
rm.deleteConfigChar()
case "enter":
if rm.configField == 0 {
maxField := 1
if rm.destination != nil && rm.destination.ManualIP {
maxField = 2
}
if rm.configField < maxField {
rm.configField++
return *m, nil
}
}
return m.startLoadingWithMessage(
"Finding Network Path",
rm.loadingDetails(*m),
rm.runAnalysis(*m),
)
return rm.submitConfig(m)
default:
if len(msg.String()) == 1 {
switch rm.configField {
case 1:
if msg.String()[0] >= '0' && msg.String()[0] <= '9' {
rm.portInput += msg.String()
}
case 2:
if strings.ContainsRune("0123456789.", rune(msg.String()[0])) {
rm.destinationIP += msg.String()
}
}
}
rm.appendConfigChar(msg.String())
}
return *m, nil
}

// configMaxField returns the last selectable config field index; manual-IP
// destinations expose an extra destination-IP field.
func (rm *reachabilityModel) configMaxField() int {
if rm.destination != nil && rm.destination.ManualIP {
return 2
}
return 1
}

func (rm *reachabilityModel) adjustConfigProtocol(delta int) {
if rm.configField != 0 {
return
}
next := rm.protocolIdx + delta
if next >= 0 && next < len(reachabilityProtocols) {
rm.protocolIdx = next
}
}

func (rm *reachabilityModel) deleteConfigChar() {
switch rm.configField {
case 1:
if len(rm.portInput) > 0 {
rm.portInput = rm.portInput[:len(rm.portInput)-1]
}
case 2:
if len(rm.destinationIP) > 0 {
rm.destinationIP = rm.destinationIP[:len(rm.destinationIP)-1]
}
}
}

func (rm *reachabilityModel) appendConfigChar(key string) {
if len(key) != 1 {
return
}
switch rm.configField {
case 1:
if key[0] >= '0' && key[0] <= '9' {
rm.portInput += key
}
case 2:
if strings.ContainsRune("0123456789.", rune(key[0])) {
rm.destinationIP += key
}
}
}

// submitConfig advances from the protocol field to the next input field, and
// starts the analysis when pressed on an input field.
func (rm *reachabilityModel) submitConfig(m *Model) (tea.Model, tea.Cmd) {
if rm.configField == 0 && rm.configField < rm.configMaxField() {
rm.configField++
return *m, nil
}
return m.startLoadingWithMessage(
"Finding Network Path",
rm.loadingDetails(*m),
rm.runAnalysis(*m),
)
}

func (rm *reachabilityModel) updateResult(m *Model, msg tea.KeyMsg) (tea.Model, tea.Cmd) {
switch msg.String() {
case "q":
Expand Down
85 changes: 85 additions & 0 deletions internal/app/screen_reachability_config_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
package app

import (
"testing"

awsservice "unic/internal/services/aws"
)

func manualIPReachabilityModel() reachabilityModel {
rm := newReachabilityModel()
rm.destination = &awsservice.ReachabilityTarget{ManualIP: true}
return rm
}

func TestReachabilityConfigMaxField(t *testing.T) {
rm := newReachabilityModel()
if rm.configMaxField() != 1 {
t.Fatalf("expected max field 1 without manual IP, got %d", rm.configMaxField())
}
rm = manualIPReachabilityModel()
if rm.configMaxField() != 2 {
t.Fatalf("expected max field 2 with manual IP destination, got %d", rm.configMaxField())
}
}

func TestReachabilityConfigProtocolAdjustClamps(t *testing.T) {
rm := newReachabilityModel()
rm.configField = 0
rm.protocolIdx = 0

rm.adjustConfigProtocol(-1)
if rm.protocolIdx != 0 {
t.Fatalf("expected protocol index to clamp at 0, got %d", rm.protocolIdx)
}
rm.adjustConfigProtocol(1)
if rm.protocolIdx != 1 {
t.Fatalf("expected protocol index 1, got %d", rm.protocolIdx)
}

rm.configField = 1
rm.adjustConfigProtocol(1)
if rm.protocolIdx != 1 {
t.Fatalf("expected protocol unchanged off the protocol field, got %d", rm.protocolIdx)
}
}

func TestReachabilityConfigCharacterInputValidation(t *testing.T) {
rm := manualIPReachabilityModel()

rm.configField = 1
rm.portInput = ""
rm.appendConfigChar("4")
rm.appendConfigChar("x")
rm.appendConfigChar("3")
if rm.portInput != "43" {
t.Fatalf("expected digits-only port input, got %q", rm.portInput)
}
rm.deleteConfigChar()
if rm.portInput != "4" {
t.Fatalf("expected backspace on port input, got %q", rm.portInput)
}

rm.configField = 2
rm.destinationIP = ""
rm.appendConfigChar("1")
rm.appendConfigChar(".")
rm.appendConfigChar("a")
if rm.destinationIP != "1." {
t.Fatalf("expected IP charset validation, got %q", rm.destinationIP)
}
}

func TestReachabilityConfigSubmitAdvancesFromProtocolField(t *testing.T) {
m := Model{screen: screenReachabilityConfig}
m.reachability = newReachabilityModel()
m.reachability.configField = 0

_, cmd := m.reachability.submitConfig(&m)
if m.reachability.configField != 1 {
t.Fatalf("expected enter on protocol field to advance, got field %d", m.reachability.configField)
}
if cmd != nil {
t.Fatal("expected no analysis to start when advancing fields")
}
}
Loading