diff --git a/cliv2/internal/cliv2/cliv2.go b/cliv2/internal/cliv2/cliv2.go index 75b1831ad4..5a67ab1b33 100644 --- a/cliv2/internal/cliv2/cliv2.go +++ b/cliv2/internal/cliv2/cliv2.go @@ -67,6 +67,13 @@ const ( ERROR_HAS_BEEN_DISPLAYED = "hasBeenDisplayed" ) +var ( + ErrIPCNotNeeded = errors.New("no IPC communication was needed") + ErrIPCNoDataSent = errors.New("no data was sent through the IPC") + ErrIPCFailedToRead = errors.New("error while reading IPC file") + ErrIPCFailedToDeserialize = errors.New("error while deserializing IPC file") +) + func NewCLIv2(config configuration.Configuration, debugLogger *log.Logger, ri runtimeinfo.RuntimeInfo) (*CLI, error) { cacheDirectory := config.GetString(configuration.CACHE_PATH) @@ -461,51 +468,55 @@ func (c *CLI) executeV1Default(proxyInfo *proxy.ProxyInfo, passThroughArgs []str return ctx.Err() } - if err == nil { - return nil - } - - if exitErr, ok := err.(*exec.ExitError); ok && exitErr.ExitCode() < 2 { + sentErr, ipcErr := GetErrorFromFile(err, filePath, c.globalConfig) + if ipcErr != nil { + if !errors.Is(ipcErr, ErrIPCNotNeeded) { + c.DebugLogger.Println("IPC: ", ipcErr.Error()) + } return err } - if sentErrs, fileErr := c.getErrorFromFile(filePath); fileErr == nil { - err = errors.Join(err, sentErrs) + return sentErr +} + +func GetErrorFromFile(execErr error, errFilePath string, config configuration.Configuration) (data error, ipcReadErr error) { + if execErr == nil { + return nil, ErrIPCNotNeeded } - return err -} + if exitErr, ok := execErr.(*exec.ExitError); ok && exitErr.ExitCode() < 2 { + return nil, ErrIPCNotNeeded + } -func (c *CLI) getErrorFromFile(errFilePath string) (data error, err error) { bytes, fileErr := os.ReadFile(errFilePath) + if os.IsNotExist(fileErr) { + return nil, ErrIPCNoDataSent + } + if fileErr != nil { - c.DebugLogger.Println("Failed to read error file: ", fileErr) - return nil, fileErr + return nil, fmt.Errorf("%w: %w", ErrIPCFailedToRead, fileErr) } jsonErrors, serErr := snyk_errors.FromJSONAPIErrorBytes(bytes) if serErr != nil { - c.DebugLogger.Println("Failed to deserialize file: ", serErr) - return nil, fileErr + return nil, fmt.Errorf("%w: %w", ErrIPCFailedToDeserialize, serErr) } if len(jsonErrors) != 0 { - hasBeenDisplayed := GetErrorDisplayStatus(c.globalConfig) + hasBeenDisplayed := GetErrorDisplayStatus(config) errs := make([]error, len(jsonErrors)+1) + errs = append(errs, execErr) for _, jerr := range jsonErrors { jerr.Meta["orign"] = "Typescript-CLI" jerr.Meta[ERROR_HAS_BEEN_DISPLAYED] = hasBeenDisplayed errs = append(errs, jerr) } - err = errors.Join(errs...) - c.DebugLogger.Println("Error file contained ", len(jsonErrors), " errors: ", err) - return err, nil + return errors.Join(errs...), nil } - c.DebugLogger.Println("The file didn't contain any errors") - return nil, errors.New("no errorrs were sent thought the error file") + return nil, ErrIPCNoDataSent } func (c *CLI) Execute(proxyInfo *proxy.ProxyInfo, passThroughArgs []string) error { diff --git a/cliv2/internal/cliv2/cliv2_test.go b/cliv2/internal/cliv2/cliv2_test.go index cf608694e7..7426e17c3e 100644 --- a/cliv2/internal/cliv2/cliv2_test.go +++ b/cliv2/internal/cliv2/cliv2_test.go @@ -14,6 +14,7 @@ import ( "testing" "time" + "github.com/snyk/error-catalog-golang-public/snyk_errors" "github.com/snyk/go-application-framework/pkg/app" "github.com/snyk/go-application-framework/pkg/configuration" "github.com/snyk/go-application-framework/pkg/local_workflows/output_workflow" @@ -587,3 +588,122 @@ func Test_GetErrorDisplayStatus(t *testing.T) { }) } } + +func Test_GetErrorFromFile(t *testing.T) { + // returns an ExitError instances with the specified exit code + getExitError := func(exitCode int) error { + var err error + command := fmt.Sprintf("exit %d", exitCode) + if runtime.GOOS == "windows" { + cmd := exec.Command("cmd", "/C", command) + err = cmd.Run() + } else { + cmd := exec.Command("sh", "-c", command) + err = cmd.Run() + } + + return err + } + + // Setup + jsonAPIErrorBytes := []byte(`{"jsonapi":{"version":"1.0"},"errors":[{"id":"1","links":{"about":"https://docs.snyk.io/scan-with-snyk/error-catalog#snyk-os-7001"},"status":"504","code":"SNYK-OS-7001","title":"Request to Snyk API timeout","detail":"connection is timedout","meta":{"links":["https://status.snyk.io/"],"isErrorCatalogError":true,"classification":"UNEXPECTED"}}]}`) + validFilePath := path.Join(t.TempDir(), "ipc-err-file") + err := os.WriteFile(validFilePath, jsonAPIErrorBytes, 0664) + assert.Nil(t, err) + + noErrorBytes := []byte(`{"jsonapi":{"version":"1.0"},"errors":[]}`) + noErrorsFilePath := path.Join(t.TempDir(), "no-err-file") + err = os.WriteFile(noErrorsFilePath, noErrorBytes, 0664) + assert.Nil(t, err) + + notJsonBytes := []byte(`this is not a valid json`) + invalidFilePath := path.Join(t.TempDir(), "invalid-err-file") + err = os.WriteFile(invalidFilePath, notJsonBytes, 0664) + assert.Nil(t, err) + + notFoundFilePath := path.Join(t.TempDir(), "not-found-file") + + jsonApiError, err := snyk_errors.FromJSONAPIErrorBytes(jsonAPIErrorBytes) + assert.Nil(t, err) + jsonAPIError := jsonApiError[0] + + config := configuration.NewWithOpts(configuration.WithAutomaticEnv()) + + t.Run("does not retrieve errors for exit code 0", func(t *testing.T) { + exitCodeErr := getExitError(0) + sentErr, err := cliv2.GetErrorFromFile(exitCodeErr, validFilePath, config) + + assert.Nil(t, sentErr) + assert.ErrorIs(t, err, cliv2.ErrIPCNotNeeded) + }) + + t.Run("does not retrieve errors for exit code 1", func(t *testing.T) { + exitCodeErr := getExitError(1) + sentErr, err := cliv2.GetErrorFromFile(exitCodeErr, validFilePath, config) + + assert.Nil(t, sentErr) + assert.ErrorIs(t, err, cliv2.ErrIPCNotNeeded) + }) + + t.Run("does not retrieve errors if the IPC was not used", func(t *testing.T) { + exitCodeErr := getExitError(2) + sentErr, err := cliv2.GetErrorFromFile(exitCodeErr, notFoundFilePath, config) + + assert.Nil(t, sentErr) + assert.ErrorIs(t, err, cliv2.ErrIPCNoDataSent) + }) + + t.Run("does not retrieve errors if the file cannot be read", func(t *testing.T) { + exitCodeErr := getExitError(2) + sentErr, err := cliv2.GetErrorFromFile(exitCodeErr, t.TempDir(), config) + + assert.Nil(t, sentErr) + assert.ErrorIs(t, err, cliv2.ErrIPCFailedToRead) + }) + + t.Run("does not retrieve errors if the IPC data cannot be deserialized", func(t *testing.T) { + exitCodeErr := getExitError(2) + sentErr, err := cliv2.GetErrorFromFile(exitCodeErr, invalidFilePath, config) + + assert.Nil(t, sentErr) + assert.ErrorIs(t, err, cliv2.ErrIPCFailedToDeserialize) + }) + + t.Run("does not retrieve errors if the IPC data cannot be deserialized", func(t *testing.T) { + exitCodeErr := getExitError(2) + sentErr, err := cliv2.GetErrorFromFile(exitCodeErr, invalidFilePath, config) + + assert.Nil(t, sentErr) + assert.ErrorIs(t, err, cliv2.ErrIPCFailedToDeserialize) + }) + + t.Run("does not retrieve errors if the IPC didnt send any errors", func(t *testing.T) { + exitCodeErr := getExitError(2) + sentErr, err := cliv2.GetErrorFromFile(exitCodeErr, noErrorsFilePath, config) + + assert.Nil(t, sentErr) + assert.ErrorIs(t, err, cliv2.ErrIPCNoDataSent) + }) + + t.Run("retrieves errors for exit code 2", func(t *testing.T) { + exitCodeErr := getExitError(2) + sentErr, err := cliv2.GetErrorFromFile(exitCodeErr, validFilePath, config) + + snykErr := snyk_errors.Error{} + assert.ErrorIs(t, sentErr, exitCodeErr) + assert.ErrorAs(t, sentErr, &snykErr) + assert.Equal(t, snykErr.ErrorCode, jsonAPIError.ErrorCode) + assert.Nil(t, err) + }) + + t.Run("retrieves errors for exit code 44", func(t *testing.T) { + exitCodeErr := getExitError(44) + sentErr, err := cliv2.GetErrorFromFile(exitCodeErr, validFilePath, config) + + snykErr := snyk_errors.Error{} + assert.ErrorIs(t, sentErr, exitCodeErr) + assert.ErrorAs(t, sentErr, &snykErr) + assert.Equal(t, snykErr.ErrorCode, jsonAPIError.ErrorCode) + assert.Nil(t, err) + }) +}