diff --git a/api/client.go b/api/client.go index 683f4b16..ca444da0 100644 --- a/api/client.go +++ b/api/client.go @@ -1,6 +1,9 @@ package api import ( + "errors" + "fmt" + "os" "time" "github.com/spf13/viper" @@ -31,9 +34,11 @@ func Client(config jira.Config) *jira.Client { config.APIToken = viper.GetString("api_token") } if config.APIToken == "" { - netrcConfig, _ := netrc.Read(config.Server, config.Login) - if netrcConfig != nil { + netrcConfig, err := netrc.Read(config.Server, config.Login) + if err == nil { config.APIToken = netrcConfig.Password + } else if !errors.Is(err, netrc.ErrNetrcEntryNotFound) { + fmt.Fprintf(os.Stderr, "warning: netrc lookup failed: %v\n", err) } } if config.APIToken == "" { diff --git a/pkg/netrc/netrc.go b/pkg/netrc/netrc.go index cd9b3033..b86a74f6 100644 --- a/pkg/netrc/netrc.go +++ b/pkg/netrc/netrc.go @@ -30,6 +30,9 @@ func Read(machine string, login string) (*Entry, error) { if err != nil { return nil, err } + if serverURL.Host == "" { + return nil, fmt.Errorf("netrc config: invalid machine URL %q: missing host", machine) + } for _, line := range netrc { if line.machine == serverURL.Host && line.login == login { diff --git a/pkg/netrc/netrc_test.go b/pkg/netrc/netrc_test.go new file mode 100644 index 00000000..7331f8c4 --- /dev/null +++ b/pkg/netrc/netrc_test.go @@ -0,0 +1,76 @@ +package netrc + +import ( + "errors" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestRead_URLParsing(t *testing.T) { + t.Parallel() + + cases := []struct { + name string + machine string + login string + wantErr bool + errContains string + }{ + { + name: "absolute path URL parses but has empty host", + machine: "/some/path", + login: "user", + wantErr: true, + errContains: "missing host", + }, + { + name: "bare hostname without scheme is invalid URI", + machine: "example.com", + login: "user", + wantErr: true, + errContains: "invalid", + }, + { + name: "valid URL with host returns not-found for unknown entry", + machine: "https://no-such-host.jira-cli-test.invalid", + login: "user@example.com", + wantErr: true, + }, + } + + for _, tc := range cases { + tc := tc + + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + entry, err := Read(tc.machine, tc.login) + require.Error(t, err) + assert.Nil(t, entry) + + if tc.errContains != "" { + assert.Contains(t, err.Error(), tc.errContains) + } + }) + } +} + +func TestRead_NotFound(t *testing.T) { + t.Parallel() + + _, err := Read("https://no-such-host.jira-cli-test.invalid", "user@example.com") + require.Error(t, err) + assert.True(t, errors.Is(err, ErrNetrcEntryNotFound), "expected ErrNetrcEntryNotFound, got: %v", err) +} + +func TestRead_EmptyHostReturnsDistinctError(t *testing.T) { + t.Parallel() + + _, err := Read("/absolute/path", "user") + require.Error(t, err) + // Should NOT be ErrNetrcEntryNotFound — it's a distinct validation error + assert.False(t, errors.Is(err, ErrNetrcEntryNotFound)) + assert.Contains(t, err.Error(), "missing host") +} diff --git a/pkg/tui/helper.go b/pkg/tui/helper.go index d35a1a7b..378df680 100644 --- a/pkg/tui/helper.go +++ b/pkg/tui/helper.go @@ -10,6 +10,7 @@ import ( "github.com/cli/safeexec" "github.com/gdamore/tcell/v2" + "github.com/google/shlex" "github.com/mattn/go-isatty" "github.com/rivo/tview" @@ -109,7 +110,10 @@ func PagerOut(out string) error { return err } - pa := strings.Split(pagerCmd, " ") + pa, err := shlex.Split(pagerCmd) + if err != nil || len(pa) == 0 { + return fmt.Errorf("invalid pager command %q: %w", pagerCmd, err) + } pager, pagerArgs := pa[0], pa[1:] if err := cmdExists(pager); err != nil { return err diff --git a/pkg/tui/helper_test.go b/pkg/tui/helper_test.go index e24f4ecc..7343c993 100644 --- a/pkg/tui/helper_test.go +++ b/pkg/tui/helper_test.go @@ -2,9 +2,11 @@ package tui import ( "os" + "runtime" "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestColumnPadding(t *testing.T) { @@ -123,6 +125,33 @@ func TestSplitText(t *testing.T) { } } +func TestPagerOut_InvalidShlexSyntax(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("pager not used on Windows") + } + + t.Setenv("TERM", "xterm") + t.Setenv("JIRA_PAGER", `less "--unclosed`) + + err := PagerOut("test output") + require.Error(t, err) + assert.Contains(t, err.Error(), "invalid pager command") +} + +func TestPagerOut_QuotedArgs(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("pager not used on Windows") + } + + // "cat" with a quoted (but valid) argument string — shlex should parse it + // without error and the command should run successfully. + t.Setenv("TERM", "xterm") + t.Setenv("JIRA_PAGER", `cat`) + + err := PagerOut("test output") + assert.NoError(t, err) +} + func TestGetPager(t *testing.T) { // TERM is xterm, JIRA_PAGER is not set, PAGER is set. {