diff --git a/errormapper/errormapper.go b/errormapper/errormapper.go new file mode 100644 index 00000000..50c888fa --- /dev/null +++ b/errormapper/errormapper.go @@ -0,0 +1,68 @@ +package errormapper + +import ( + "regexp" + + "github.com/bitrise-io/bitrise-init/step" +) + +const ( + // UnknownParam ... + UnknownParam = "::unknown::" + // DetailedErrorRecKey ... + DetailedErrorRecKey = "DetailedError" +) + +// DetailedError ... +type DetailedError struct { + Title string + Description string +} + +// NewDetailedErrorRecommendation ... +func NewDetailedErrorRecommendation(detailedError DetailedError) step.Recommendation { + return step.Recommendation{ + DetailedErrorRecKey: detailedError, + } +} + +// DetailedErrorBuilder ... +type DetailedErrorBuilder = func(...string) DetailedError + +// GetParamAt ... +func GetParamAt(index int, params []string) string { + res := UnknownParam + if index >= 0 && len(params) > index { + res = params[index] + } + return res +} + +// PatternToDetailedErrorBuilder ... +type PatternToDetailedErrorBuilder map[string]DetailedErrorBuilder + +// PatternErrorMatcher ... +type PatternErrorMatcher struct { + DefaultBuilder DetailedErrorBuilder + PatternToBuilder PatternToDetailedErrorBuilder +} + +// Run ... +func (m *PatternErrorMatcher) Run(msg string) step.Recommendation { + for pattern, builder := range m.PatternToBuilder { + re := regexp.MustCompile(pattern) + if re.MatchString(msg) { + // [search_string, match1, match2, ...] + matches := re.FindStringSubmatch((msg)) + // Drop the first item, which is always the search_string itself + // [search_string] -> [] + // [search_string, match1, ...] -> [match1, ...] + params := matches[1:] + detail := builder(params...) + return NewDetailedErrorRecommendation(detail) + } + } + + detail := m.DefaultBuilder(msg) + return NewDetailedErrorRecommendation(detail) +} diff --git a/errormapper/errormapper_test.go b/errormapper/errormapper_test.go new file mode 100644 index 00000000..6b3afac8 --- /dev/null +++ b/errormapper/errormapper_test.go @@ -0,0 +1,225 @@ +package errormapper + +import ( + "fmt" + "reflect" + "testing" + + "github.com/bitrise-io/bitrise-init/step" +) + +func Test_newDetailedErrorRecommendation(t *testing.T) { + type args struct { + detailedError DetailedError + } + tests := []struct { + name string + args args + want step.Recommendation + }{ + { + name: "newDetailedErrorRecommendation with nil", + args: args{ + detailedError: DetailedError{ + Title: "TestTitle", + Description: "TestDesciption", + }, + }, + want: step.Recommendation{ + DetailedErrorRecKey: DetailedError{ + Title: "TestTitle", + Description: "TestDesciption", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := NewDetailedErrorRecommendation(tt.args.detailedError); !reflect.DeepEqual(got, tt.want) { + t.Errorf("newDetailedErrorRecommendation() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_getParamAt(t *testing.T) { + type args struct { + index int + params []string + } + tests := []struct { + name string + args args + want string + }{ + { + name: "getParamsAt(0, nil)", + args: args{ + index: 0, + params: nil, + }, + want: UnknownParam, + }, + { + name: "getParamsAt(0, [])", + args: args{ + index: 0, + params: []string{}, + }, + want: UnknownParam, + }, + { + name: "getParamsAt(-1, ['1', '2', '3', '4', '5'])", + args: args{ + index: -1, + params: []string{"1", "2", "3", "4", "5"}, + }, + want: UnknownParam, + }, + { + name: "getParamsAt(5, ['1', '2', '3', '4', '5'])", + args: args{ + index: 5, + params: []string{"1", "2", "3", "4", "5"}, + }, + want: UnknownParam, + }, + { + name: "getParamsAt(0, ['1', '2', '3', '4', '5'])", + args: args{ + index: 0, + params: []string{"1", "2", "3", "4", "5"}, + }, + want: "1", + }, + { + name: "getParamsAt(4, ['1', '2', '3', '4', '5'])", + args: args{ + index: 4, + params: []string{"1", "2", "3", "4", "5"}, + }, + want: "5", + }, + { + name: "getParamsAt(2, ['1', '2', '3', '4', '5'])", + args: args{ + index: 2, + params: []string{"1", "2", "3", "4", "5"}, + }, + want: "3", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := GetParamAt(tt.args.index, tt.args.params); got != tt.want { + t.Errorf("getParamAt() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestPatternErrorMatcher_Run(t *testing.T) { + type fields struct { + defaultBuilder DetailedErrorBuilder + patternToBuilder PatternToDetailedErrorBuilder + } + type args struct { + msg string + } + tests := []struct { + name string + fields fields + args args + want step.Recommendation + }{ + { + name: "Run with defaultBuilder", + fields: fields{ + defaultBuilder: func(params ...string) DetailedError { + return DetailedError{ + Title: "T", + Description: "D", + } + }, + patternToBuilder: map[string]DetailedErrorBuilder{}, + }, + args: args{ + msg: "Test", + }, + want: step.Recommendation{ + DetailedErrorRecKey: DetailedError{ + Title: "T", + Description: "D", + }, + }, + }, + { + name: "Run with patternBuilder", + fields: fields{ + defaultBuilder: func(params ...string) DetailedError { + return DetailedError{ + Title: "DefaultTitle", + Description: "DefaultDesc", + } + }, + patternToBuilder: map[string]DetailedErrorBuilder{ + "Test": func(params ...string) DetailedError { + return DetailedError{ + Title: "PatternTitle", + Description: "PatternDesc", + } + }, + }, + }, + args: args{ + msg: "Test", + }, + want: step.Recommendation{ + DetailedErrorRecKey: DetailedError{ + Title: "PatternTitle", + Description: "PatternDesc", + }, + }, + }, + { + name: "Run with patternBuilder with param", + fields: fields{ + defaultBuilder: func(params ...string) DetailedError { + return DetailedError{ + Title: "DefaultTitle", + Description: "DefaultDesc", + } + }, + patternToBuilder: map[string]DetailedErrorBuilder{ + "Test (.+)!": func(params ...string) DetailedError { + p := GetParamAt(0, params) + return DetailedError{ + Title: "PatternTitle", + Description: fmt.Sprintf("PatternDesc: '%s'", p), + } + }, + }, + }, + args: args{ + msg: "Test WithPatternParam!", + }, + want: step.Recommendation{ + DetailedErrorRecKey: DetailedError{ + Title: "PatternTitle", + Description: "PatternDesc: 'WithPatternParam'", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + m := &PatternErrorMatcher{ + DefaultBuilder: tt.fields.defaultBuilder, + PatternToBuilder: tt.fields.patternToBuilder, + } + if got := m.Run(tt.args.msg); !reflect.DeepEqual(got, tt.want) { + t.Errorf("PatternErrorMatcher.Run() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/gitclone/git.go b/gitclone/git.go index 46d6e637..63b6b5cb 100644 --- a/gitclone/git.go +++ b/gitclone/git.go @@ -21,6 +21,13 @@ import ( "github.com/bitrise-io/go-utils/pathutil" "github.com/bitrise-io/go-utils/retry" "github.com/bitrise-io/go-utils/sliceutil" + "github.com/bitrise-steplib/steps-git-clone/errormapper" +) + +const ( + checkoutFailedTag = "checkout_failed" + fetchFailedTag = "fetch_failed" + branchRecKey = "BranchRecommendation" ) func isOriginPresent(gitCmd git.Git, dir, repoURL string) (bool, error) { @@ -291,14 +298,14 @@ func manualMerge(gitCmd git.Git, repoURL, prRepoURL, branch, commit, branchDest func parseListBranchesOutput(output string) map[string][]string { lines := strings.Split(output, "\n") - branchesByRemote := map[string][]string {} + branchesByRemote := map[string][]string{} for _, line := range lines { line = strings.Trim(line, " ") split := strings.Split(line, "/") remote := split[0] branch := "" - if (len(split) > 1) { + if len(split) > 1 { branch = strings.Join(split[1:], "/") branches := branchesByRemote[remote] branches = append(branches, branch) @@ -339,17 +346,18 @@ func checkout(gitCmd git.Git, arg, branch string, depth int, isTag bool) *step.E branches := branchesByRemote[defaultRemoteName] if branchesErr == nil && !sliceutil.IsStringInSlice(branch, branches) { return newStepErrorWithRecommendations( - "fetch_failed", + fetchFailedTag, fmt.Errorf("fetch failed: invalid branch selected: %s, available branches: %s: %v", branch, strings.Join(branches, ", "), err), "Fetching repository has failed", step.Recommendation{ - "BranchRecommendation": branches, + branchRecKey: branches, + errormapper.DetailedErrorRecKey: newFetchFailedInvalidBranchDetailedError(branch), }, ) } } return newStepError( - "fetch_failed", + fetchFailedTag, fmt.Errorf("fetch failed, error: %v", err), "Fetching repository has failed", ) @@ -358,7 +366,7 @@ func checkout(gitCmd git.Git, arg, branch string, depth int, isTag bool) *step.E if err := run(gitCmd.Checkout(arg)); err != nil { if depth == 0 { return newStepError( - "checkout_failed", + checkoutFailedTag, fmt.Errorf("checkout failed (%s), error: %v", arg, err), "Checkout has failed", ) diff --git a/gitclone/gitclone.go b/gitclone/gitclone.go index 97f88b32..811f71ef 100644 --- a/gitclone/gitclone.go +++ b/gitclone/gitclone.go @@ -32,8 +32,9 @@ type Config struct { } const ( - trimEnding = "..." - defaultRemoteName = "origin" + trimEnding = "..." + defaultRemoteName = "origin" + updateSubmodelFailedTag = "update_submodule_failed" ) func printLogAndExportEnv(gitCmd git.Git, format, env string, maxEnvLength int) error { @@ -160,7 +161,7 @@ func Execute(cfg Config) *step.Error { if cfg.UpdateSubmodules { if err := run(gitCmd.SubmoduleUpdate()); err != nil { return newStepError( - "update_submodule_failed", + updateSubmodelFailedTag, fmt.Errorf("submodule update: %v", err), "Updating submodules has failed", ) diff --git a/gitclone/steperror.go b/gitclone/steperror.go index 73e1cba7..840c36f2 100644 --- a/gitclone/steperror.go +++ b/gitclone/steperror.go @@ -1,11 +1,131 @@ package gitclone -import "github.com/bitrise-io/bitrise-init/step" +import ( + "fmt" + + "github.com/bitrise-io/bitrise-init/step" + "github.com/bitrise-steplib/steps-git-clone/errormapper" +) + +func mapRecommendation(tag, errMsg string) step.Recommendation { + switch tag { + case checkoutFailedTag: + matcher := newCheckoutFailedPatternErrorMatcher() + return matcher.Run(errMsg) + case updateSubmodelFailedTag: // update_submodule_failed could have the same errors as fetch + fallthrough + case fetchFailedTag: + fetchFailedMatcher := newFetchFailedPatternErrorMatcher() + return fetchFailedMatcher.Run(errMsg) + } + return nil +} func newStepError(tag string, err error, shortMsg string) *step.Error { + recommendation := mapRecommendation(tag, err.Error()) + if recommendation != nil { + return step.NewErrorWithRecommendations("git-clone", tag, err, shortMsg, recommendation) + } + return step.NewError("git-clone", tag, err, shortMsg) } func newStepErrorWithRecommendations(tag string, err error, shortMsg string, recommendations step.Recommendation) *step.Error { return step.NewErrorWithRecommendations("git-clone", tag, err, shortMsg, recommendations) } + +func newCheckoutFailedPatternErrorMatcher() *errormapper.PatternErrorMatcher { + return &errormapper.PatternErrorMatcher{ + DefaultBuilder: newCheckoutFailedGenericDetailedError, + PatternToBuilder: nil, + } +} + +func newFetchFailedPatternErrorMatcher() *errormapper.PatternErrorMatcher { + return &errormapper.PatternErrorMatcher{ + DefaultBuilder: newFetchFailedGenericDetailedError, + PatternToBuilder: errormapper.PatternToDetailedErrorBuilder{ + `Permission denied \((.+)\)`: newFetchFailedSSHAccessErrorDetailedError, + `fatal: repository '(.+)' not found`: newFetchFailedCouldNotFindGitRepoDetailedError, + `fatal: '(.+)' does not appear to be a git repository`: newFetchFailedCouldNotFindGitRepoDetailedError, + `fatal: (.+)/info/refs not valid: is this a git repository?`: newFetchFailedCouldNotFindGitRepoDetailedError, + `remote: HTTP Basic: Access denied[\n]*fatal: Authentication failed for '(.+)'`: newFetchFailedHTTPAccessErrorDetailedError, + `remote: Invalid username or password\(\.\)[\n]*fatal: Authentication failed for '(.+)'`: newFetchFailedHTTPAccessErrorDetailedError, + `Unauthorized`: newFetchFailedHTTPAccessErrorDetailedError, + `Forbidden`: newFetchFailedHTTPAccessErrorDetailedError, + `remote: Unauthorized LoginAndPassword`: newFetchFailedHTTPAccessErrorDetailedError, + // `fatal: unable to access '(.+)': Failed to connect to .+ port \d+: Connection timed out + // `fatal: unable to access '(.+)': The requested URL returned error: 400` + // `fatal: unable to access '(.+)': The requested URL returned error: 403` + `fatal: unable to access '(.+)': (Failed|The requested URL returned error: \d+)`: newFetchFailedHTTPAccessErrorDetailedError, + // `ssh: connect to host (.+) port \d+: Connection timed out` + // `ssh: connect to host (.+) port \d+: Connection refused` + // `ssh: connect to host (.+) port \d+: Network is unreachable` + `ssh: connect to host (.+) port \d+:`: newFetchFailedCouldConnectErrorDetailedError, + `ssh: Could not resolve hostname (.+): Name or service not known`: newFetchFailedCouldConnectErrorDetailedError, + `fatal: unable to access '.+': Could not resolve host: (\S+)`: newFetchFailedCouldConnectErrorDetailedError, + `ERROR: The \x60(.+)' organization has enabled or enforced SAML SSO`: newFetchFailedSamlSSOEnforcedDetailedError, + }, + } +} + +func newCheckoutFailedGenericDetailedError(params ...string) errormapper.DetailedError { + err := errormapper.GetParamAt(0, params) + return errormapper.DetailedError{ + Title: "We couldn’t checkout your branch.", + Description: fmt.Sprintf("Our auto-configurator returned the following error:\n%s", err), + } +} + +func newFetchFailedGenericDetailedError(params ...string) errormapper.DetailedError { + err := errormapper.GetParamAt(0, params) + return errormapper.DetailedError{ + Title: "We couldn’t fetch your repository.", + Description: fmt.Sprintf("Our auto-configurator returned the following error:\n%s", err), + } +} + +func newFetchFailedSSHAccessErrorDetailedError(params ...string) errormapper.DetailedError { + return errormapper.DetailedError{ + Title: "We couldn’t access your repository.", + Description: "Please abort the process, double-check your SSH key and try again.", + } +} + +func newFetchFailedCouldNotFindGitRepoDetailedError(params ...string) errormapper.DetailedError { + repoURL := errormapper.GetParamAt(0, params) + return errormapper.DetailedError{ + Title: fmt.Sprintf("We couldn’t find a git repository at '%s'.", repoURL), + Description: "Please abort the process, double-check your repository URL and try again.", + } +} + +func newFetchFailedHTTPAccessErrorDetailedError(params ...string) errormapper.DetailedError { + return errormapper.DetailedError{ + Title: "We couldn’t access your repository.", + Description: "Please abort the process and try again, by providing the repository with SSH URL.", + } +} + +func newFetchFailedCouldConnectErrorDetailedError(params ...string) errormapper.DetailedError { + host := errormapper.GetParamAt(0, params) + return errormapper.DetailedError{ + Title: fmt.Sprintf("We couldn’t connect to '%s'.", host), + Description: "Please abort the process, double-check your repository URL and try again.", + } +} + +func newFetchFailedSamlSSOEnforcedDetailedError(params ...string) errormapper.DetailedError { + return errormapper.DetailedError{ + Title: "To access this repository, you need to use SAML SSO.", + Description: `Please abort the process, update your SSH settings and try again. You can find out more about using SAML SSO in the Github docs.`, + } +} + +func newFetchFailedInvalidBranchDetailedError(params ...string) errormapper.DetailedError { + branch := errormapper.GetParamAt(0, params) + return errormapper.DetailedError{ + Title: fmt.Sprintf("We couldn't find the branch '%s'.", branch), + Description: "Please choose another branch and try again.", + } +} diff --git a/gitclone/steperror_test.go b/gitclone/steperror_test.go new file mode 100644 index 00000000..8d0bcebb --- /dev/null +++ b/gitclone/steperror_test.go @@ -0,0 +1,357 @@ +package gitclone + +import ( + "errors" + "reflect" + "testing" + + "github.com/bitrise-io/bitrise-init/step" + "github.com/bitrise-steplib/steps-git-clone/errormapper" +) + +var mapRecommendationMock func(tag, errMsg string) step.Recommendation + +func Test_mapRecommendation(t *testing.T) { + type args struct { + tag string + errMsg string + } + tests := []struct { + name string + args args + want step.Recommendation + }{ + { + name: "checkout_failed generic error mapping", + args: args{ + tag: checkoutFailedTag, + errMsg: "error: pathspec 'master' did not match any file(s) known to git.", + }, + want: errormapper.NewDetailedErrorRecommendation(errormapper.DetailedError{ + Title: "We couldn’t checkout your branch.", + Description: "Our auto-configurator returned the following error:\nerror: pathspec 'master' did not match any file(s) known to git.", + }), + }, + { + name: "fetch_failed generic error mapping", + args: args{ + tag: fetchFailedTag, + errMsg: "fetch failed, error: exit status 128", + }, + want: errormapper.NewDetailedErrorRecommendation(errormapper.DetailedError{ + Title: "We couldn’t fetch your repository.", + Description: "Our auto-configurator returned the following error:\nfetch failed, error: exit status 128", + }), + }, + { + name: "fetch_failed permission denied (publickey) error mapping", + args: args{ + tag: fetchFailedTag, + errMsg: "Permission denied (publickey).", + }, + want: errormapper.NewDetailedErrorRecommendation(errormapper.DetailedError{ + Title: "We couldn’t access your repository.", + Description: "Please abort the process, double-check your SSH key and try again.", + }), + }, + { + name: "fetch_failed permission denied (publickey,publickey,gssapi-keyex,gssapi-with-mic,password) error mapping", + args: args{ + tag: fetchFailedTag, + errMsg: "Permission denied (publickey,gssapi-keyex,gssapi-with-mic,password).", + }, + want: errormapper.NewDetailedErrorRecommendation(errormapper.DetailedError{ + Title: "We couldn’t access your repository.", + Description: "Please abort the process, double-check your SSH key and try again.", + }), + }, + { + name: "fetch_failed could not find repository (fatal: repository 'http://localhost/repo.git' not found) error mapping", + args: args{ + tag: fetchFailedTag, + errMsg: "fatal: repository 'http://localhost/repo.git' not found", + }, + want: errormapper.NewDetailedErrorRecommendation(errormapper.DetailedError{ + Title: "We couldn’t find a git repository at 'http://localhost/repo.git'.", + Description: "Please abort the process, double-check your repository URL and try again.", + }), + }, + { + name: "fetch_failed could not find repository (fatal: 'totally.not.made.up' does not appear to be a git repository) error mapping", + args: args{ + tag: fetchFailedTag, + errMsg: "fatal: 'totally.not.made.up' does not appear to be a git repository", + }, + want: errormapper.NewDetailedErrorRecommendation(errormapper.DetailedError{ + Title: "We couldn’t find a git repository at 'totally.not.made.up'.", + Description: "Please abort the process, double-check your repository URL and try again.", + }), + }, + { + name: "fetch_failed could not find repository (fatal: https://www.youtube.com/channel/UCh0BVQAUkD3vr3WzmINFO5A/info/refs not valid: is this a git repository?) error mapping", + args: args{ + tag: fetchFailedTag, + errMsg: "fatal: https://www.youtube.com/channel/UCh0BVQAUkD3vr3WzmINFO5A/info/refs not valid: is this a git repository?", + }, + want: errormapper.NewDetailedErrorRecommendation(errormapper.DetailedError{ + Title: "We couldn’t find a git repository at 'https://www.youtube.com/channel/UCh0BVQAUkD3vr3WzmINFO5A'.", + Description: "Please abort the process, double-check your repository URL and try again.", + }), + }, + { + name: "fetch_failed could not access repository (remote: HTTP Basic: Access denied\nfatal: Authentication failed for 'https://localhost/repo.git') error mapping", + args: args{ + tag: fetchFailedTag, + errMsg: "remote: HTTP Basic: Access denied\nfatal: Authentication failed for 'https://localhost/repo.git'", + }, + want: errormapper.NewDetailedErrorRecommendation(errormapper.DetailedError{ + Title: "We couldn’t access your repository.", + Description: "Please abort the process and try again, by providing the repository with SSH URL.", + }), + }, + { + name: "fetch_failed could not access repository (remote: Invalid username or password(.)\nfatal: Authentication failed for 'https://localhost/repo.git') error mapping", + args: args{ + tag: fetchFailedTag, + errMsg: "remote: Invalid username or password(.)\nfatal: Authentication failed for 'https://localhost/repo.git'", + }, + want: errormapper.NewDetailedErrorRecommendation(errormapper.DetailedError{ + Title: "We couldn’t access your repository.", + Description: "Please abort the process and try again, by providing the repository with SSH URL.", + }), + }, + { + name: "fetch_failed could not access repository (Unauthorized) error mapping", + args: args{ + tag: fetchFailedTag, + errMsg: "Unauthorized", + }, + want: errormapper.NewDetailedErrorRecommendation(errormapper.DetailedError{ + Title: "We couldn’t access your repository.", + Description: "Please abort the process and try again, by providing the repository with SSH URL.", + }), + }, + { + name: "fetch_failed could not access repository (Forbidden) error mapping", + args: args{ + tag: fetchFailedTag, + errMsg: "Forbidden", + }, + want: errormapper.NewDetailedErrorRecommendation(errormapper.DetailedError{ + Title: "We couldn’t access your repository.", + Description: "Please abort the process and try again, by providing the repository with SSH URL.", + }), + }, + { + name: "fetch_failed could not access repository (fatal: unable to access 'https://git.something.com/group/repo.git/': Failed to connect to git.something.com port 443: Connection timed out) error mapping", + args: args{ + tag: fetchFailedTag, + errMsg: "fatal: unable to access 'https://git.something.com/group/repo.git/': Failed to connect to git.something.com port 443: Connection timed out", + }, + want: errormapper.NewDetailedErrorRecommendation(errormapper.DetailedError{ + Title: "We couldn’t access your repository.", + Description: "Please abort the process and try again, by providing the repository with SSH URL.", + }), + }, + { + name: "fetch_failed could not access repository (fatal: unable to access 'https://github.com/group/repo.git)/': The requested URL returned error: 400) error mapping", + args: args{ + tag: fetchFailedTag, + errMsg: "fatal: unable to access 'https://github.com/group/repo.git)/': The requested URL returned error: 400", + }, + want: errormapper.NewDetailedErrorRecommendation(errormapper.DetailedError{ + Title: "We couldn’t access your repository.", + Description: "Please abort the process and try again, by providing the repository with SSH URL.", + }), + }, + { + name: "fetch_failed could not connect (ssh: connect to host git.something.com.outer port 22: Connection timed out) error mapping", + args: args{ + tag: fetchFailedTag, + errMsg: "ssh: connect to host git.something.com port 22: Connection timed out) error mapping", + }, + want: errormapper.NewDetailedErrorRecommendation(errormapper.DetailedError{ + Title: "We couldn’t connect to 'git.something.com'.", + Description: "Please abort the process, double-check your repository URL and try again.", + }), + }, + { + name: "fetch_failed could not connect (ssh: connect to host git.something.com.outer port 22: Connection refused) error mapping", + args: args{ + tag: fetchFailedTag, + errMsg: "ssh: connect to host git.something.com port 22: Connection refused) error mapping", + }, + want: errormapper.NewDetailedErrorRecommendation(errormapper.DetailedError{ + Title: "We couldn’t connect to 'git.something.com'.", + Description: "Please abort the process, double-check your repository URL and try again.", + }), + }, + { + name: "fetch_failed could not connect (ssh: connect to host git.something.com.outer port 22: Network is unreachable) error mapping", + args: args{ + tag: fetchFailedTag, + errMsg: "ssh: connect to host git.something.com port 22: Network is unreachable) error mapping", + }, + want: errormapper.NewDetailedErrorRecommendation(errormapper.DetailedError{ + Title: "We couldn’t connect to 'git.something.com'.", + Description: "Please abort the process, double-check your repository URL and try again.", + }), + }, + { + name: "fetch_failed could not connect (ssh: Could not resolve hostname git.something.com: Name or service not known) error mapping", + args: args{ + tag: fetchFailedTag, + errMsg: "ssh: Could not resolve hostname git.something.com: Name or service not known", + }, + want: errormapper.NewDetailedErrorRecommendation(errormapper.DetailedError{ + Title: "We couldn’t connect to 'git.something.com'.", + Description: "Please abort the process, double-check your repository URL and try again.", + }), + }, + { + name: "fetch_failed could not connect (fatal: unable to access 'https://site.google.com/view/something/': Could not resolve host: site.google.com) error mapping", + args: args{ + tag: fetchFailedTag, + errMsg: "fatal: unable to access 'https://site.google.com/view/something/': Could not resolve host: site.google.com"}, + want: errormapper.NewDetailedErrorRecommendation(errormapper.DetailedError{ + Title: "We couldn’t connect to 'site.google.com'.", + Description: "Please abort the process, double-check your repository URL and try again.", + }), + }, + { + name: "fetch_failed SAML SSO enforced (ERROR: The `my-company' organization has enabled or enforced SAML SSO) error mapping", + args: args{ + tag: fetchFailedTag, + errMsg: "ERROR: The `my-company' organization has enabled or enforced SAML SSO", + }, + want: errormapper.NewDetailedErrorRecommendation(errormapper.DetailedError{ + Title: "To access this repository, you need to use SAML SSO.", + Description: `Please abort the process, update your SSH settings and try again. You can find out more about using SAML SSO in the Github docs.`, + }), + }, + { + name: "update_submodule_failed generic (fatal: no submodule mapping found in .gitmodules for path 'web') error mapping", + args: args{ + tag: updateSubmodelFailedTag, + errMsg: "fatal: no submodule mapping found in .gitmodules for path 'web'", + }, + want: errormapper.NewDetailedErrorRecommendation(errormapper.DetailedError{ + Title: "We couldn’t fetch your repository.", + Description: "Our auto-configurator returned the following error:\nfatal: no submodule mapping found in .gitmodules for path 'web'", + }), + }, + { + name: "update_submodule_failed (remote: Unauthorized LoginAndPassword(Username for 'https/***): User not found) error mapping", + args: args{ + tag: updateSubmodelFailedTag, + errMsg: "remote: Unauthorized LoginAndPassword(Username for 'https/***): User not found", + }, + want: errormapper.NewDetailedErrorRecommendation(errormapper.DetailedError{ + Title: "We couldn’t access your repository.", + Description: "Please abort the process and try again, by providing the repository with SSH URL.", + }), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := mapRecommendation(tt.args.tag, tt.args.errMsg); !reflect.DeepEqual(got, tt.want) { + t.Errorf("mapRecommendation() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_newStepError(t *testing.T) { + type args struct { + tag string + err error + shortMsg string + } + tests := []struct { + name string + args args + want *step.Error + }{ + { + name: "newStepError without recommendation", + args: args{ + tag: "test_tag", + err: errors.New("fatal error"), + shortMsg: "unknown error", + }, + want: &step.Error{ + StepID: "git-clone", + Tag: "test_tag", + Err: errors.New("fatal error"), + ShortMsg: "unknown error", + }, + }, + { + name: "newStepError with recommendation", + args: args{ + tag: "fetch_failed", + err: errors.New("Permission denied (publickey)"), + shortMsg: "unknown error", + }, + want: &step.Error{ + StepID: "git-clone", + Tag: "fetch_failed", + Err: errors.New("Permission denied (publickey)"), + ShortMsg: "unknown error", + Recommendations: errormapper.NewDetailedErrorRecommendation(errormapper.DetailedError{ + Title: "We couldn’t access your repository.", + Description: "Please abort the process, double-check your SSH key and try again.", + }), + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := newStepError(tt.args.tag, tt.args.err, tt.args.shortMsg); !reflect.DeepEqual(got, tt.want) { + t.Errorf("newStepError() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_newStepErrorWithRecommendations(t *testing.T) { + type args struct { + tag string + err error + shortMsg string + recommendations step.Recommendation + } + tests := []struct { + name string + args args + want *step.Error + }{ + { + name: "newStepErrorWithRecommendations", + args: args{ + tag: "test_tag", + err: errors.New("fatal error"), + shortMsg: "unknown error", + recommendations: step.Recommendation{ + "Test": "Passed", + }, + }, + want: &step.Error{ + StepID: "git-clone", + Tag: "test_tag", + Err: errors.New("fatal error"), + ShortMsg: "unknown error", + Recommendations: step.Recommendation{ + "Test": "Passed", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := newStepErrorWithRecommendations(tt.args.tag, tt.args.err, tt.args.shortMsg, tt.args.recommendations); !reflect.DeepEqual(got, tt.want) { + t.Errorf("newStepErrorWithRecommendations() = %v, want %v", got, tt.want) + } + }) + } +}