Update configuration process and add unit tests

Amine Hilaly created

Update launchpad bridge

Change summary

bridge/github/config.go      |  50 ++++++++----
bridge/github/config_test.go | 146 ++++++++++++++++++++++++++++++++++++++
bridge/launchpad/config.go   |   9 +
3 files changed, 187 insertions(+), 18 deletions(-)

Detailed changes

bridge/github/config.go 🔗

@@ -16,6 +16,8 @@ import (
 	"syscall"
 	"time"
 
+	"github.com/pkg/errors"
+
 	"golang.org/x/crypto/ssh/terminal"
 
 	"github.com/MichaelMure/git-bug/bridge/core"
@@ -32,7 +34,7 @@ const (
 )
 
 var (
-	rxGithubURL = regexp.MustCompile(`github\.com[\/:]([a-zA-Z0-9\-\_]+)\/([a-zA-Z0-9\-\_\.]+)`)
+	ErrBadProjectURL = errors.New("bad project url")
 )
 
 func (*Github) Configure(repo repository.RepoCommon, params core.BridgeParams) (core.Configuration, error) {
@@ -50,7 +52,7 @@ func (*Github) Configure(repo repository.RepoCommon, params core.BridgeParams) (
 
 	} else if params.URL != "" {
 		// try to parse params URL and extract owner and project
-		_, owner, project, err = splitURL(params.URL)
+		owner, project, err = splitURL(params.URL)
 		if err != nil {
 			return nil, err
 		}
@@ -227,9 +229,9 @@ func promptToken() (string, error) {
 	fmt.Println()
 	fmt.Println("The access scope depend on the type of repository.")
 	fmt.Println("Public:")
-	fmt.Println("  - 'repo:public_repo': to be able to read public repositories")
+	fmt.Println("  - 'public_repo': to be able to read public repositories")
 	fmt.Println("Private:")
-	fmt.Println("  - 'repo'            : to be able to read private repositories")
+	fmt.Println("  - 'repo'       : to be able to read private repositories")
 	fmt.Println()
 
 	for {
@@ -255,9 +257,9 @@ func loginAndRequestToken(owner, project string) (string, error) {
 	fmt.Println()
 	fmt.Println("The access scope depend on the type of repository.")
 	fmt.Println("Public:")
-	fmt.Println("  - 'repo:public_repo': to be able to read public repositories")
+	fmt.Println("  - 'public_repo': to be able to read public repositories")
 	fmt.Println("Private:")
-	fmt.Println("  - 'repo'            : to be able to read private repositories")
+	fmt.Println("  - 'repo'       : to be able to read private repositories")
 	fmt.Println()
 
 	// prompt project visibility to know the token scope needed for the repository
@@ -278,8 +280,8 @@ func loginAndRequestToken(owner, project string) (string, error) {
 
 	var scope string
 	if isPublic {
-		// repo:public_repo is requested to be able to read public repositories
-		scope = "repo:public_repo"
+		// public_repo is requested to be able to read public repositories
+		scope = "public_repo"
 	} else {
 		// 'repo' is request to be able to read private repositories
 		// /!\ token will have read/write rights on every private repository you have access to
@@ -377,7 +379,7 @@ func promptURL(remotes map[string]string) (string, string, error) {
 			}
 
 			// get owner and project with index
-			_, owner, project, _ := splitURL(validRemotes[index-1])
+			owner, project, _ := splitURL(validRemotes[index-1])
 			return owner, project, nil
 		}
 	}
@@ -398,7 +400,7 @@ func promptURL(remotes map[string]string) (string, string, error) {
 		}
 
 		// get owner and project from url
-		_, owner, project, err := splitURL(line)
+		owner, project, err := splitURL(line)
 		if err != nil {
 			fmt.Println(err)
 			continue
@@ -408,22 +410,34 @@ func promptURL(remotes map[string]string) (string, string, error) {
 	}
 }
 
-func splitURL(url string) (shortURL string, owner string, project string, err error) {
+// splitURL extract the owner and project from a github repository URL. It will remove the
+// '.git' extension from the URL before parsing it.
+// Note that Github removes the '.git' extension from projects names at their creation
+func splitURL(url string) (owner string, project string, err error) {
 	cleanURL := strings.TrimSuffix(url, ".git")
-	res := rxGithubURL.FindStringSubmatch(cleanURL)
+
+	re, err := regexp.Compile(`github\.com[/:]([a-zA-Z0-9\-_]+)/([a-zA-Z0-9\-_.]+)`)
+	if err != nil {
+		return "", "", err
+	}
+
+	res := re.FindStringSubmatch(cleanURL)
 	if res == nil {
-		return "", "", "", fmt.Errorf("bad github project url")
+		return "", "", ErrBadProjectURL
 	}
 
-	return res[0], res[1], res[2], nil
+	owner = res[1]
+	project = res[2]
+	return owner, project, nil
 }
 
 func getValidGithubRemoteURLs(remotes map[string]string) []string {
 	urls := make([]string, 0, len(remotes))
 	for _, url := range remotes {
 		// split url can work again with shortURL
-		shortURL, _, _, err := splitURL(url)
+		owner, project, err := splitURL(url)
 		if err == nil {
+			shortURL := fmt.Sprintf("%s/%s/%s", "github.com", owner, project)
 			urls = append(urls, shortURL)
 		}
 	}
@@ -434,7 +448,11 @@ func getValidGithubRemoteURLs(remotes map[string]string) []string {
 func validateUsername(username string) (bool, error) {
 	url := fmt.Sprintf("%s/users/%s", githubV3Url, username)
 
-	resp, err := http.Get(url)
+	client := &http.Client{
+		Timeout: defaultTimeout,
+	}
+
+	resp, err := client.Get(url)
 	if err != nil {
 		return false, err
 	}

bridge/github/config_test.go 🔗

@@ -0,0 +1,146 @@
+package github
+
+import (
+	"os"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestSplitURL(t *testing.T) {
+	type args struct {
+		url string
+	}
+	type want struct {
+		owner   string
+		project string
+		err     error
+	}
+	tests := []struct {
+		name string
+		args args
+		want want
+	}{
+		{
+			name: "default url",
+			args: args{
+				url: "https://github.com/MichaelMure/git-bug",
+			},
+			want: want{
+				owner:   "MichaelMure",
+				project: "git-bug",
+				err:     nil,
+			},
+		},
+
+		{
+			name: "default url with git extension",
+			args: args{
+				url: "https://github.com/MichaelMure/git-bug.git",
+			},
+			want: want{
+				owner:   "MichaelMure",
+				project: "git-bug",
+				err:     nil,
+			},
+		},
+		{
+			name: "url with git protocol",
+			args: args{
+				url: "git://github.com/MichaelMure/git-bug.git",
+			},
+			want: want{
+				owner:   "MichaelMure",
+				project: "git-bug",
+				err:     nil,
+			},
+		},
+		{
+			name: "ssh url",
+			args: args{
+				url: "git@github.com:MichaelMure/git-bug.git",
+			},
+			want: want{
+				owner:   "MichaelMure",
+				project: "git-bug",
+				err:     nil,
+			},
+		},
+		{
+			name: "bad url",
+			args: args{
+				url: "https://githb.com/MichaelMure/git-bug.git",
+			},
+			want: want{
+				err: ErrBadProjectURL,
+			},
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			owner, project, err := splitURL(tt.args.url)
+			assert.Equal(t, tt.want.err, err)
+			assert.Equal(t, tt.want.owner, owner)
+			assert.Equal(t, tt.want.project, project)
+		})
+	}
+}
+
+func TestValidateProject(t *testing.T) {
+	tokenPrivateScope := os.Getenv("GITHUB_TOKEN_PRIVATE")
+	if tokenPrivateScope == "" {
+		t.Skip("Env var GITHUB_TOKEN_PRIVATE missing")
+	}
+
+	tokenPublicScope := os.Getenv("GITHUB_TOKEN_PUBLIC")
+	if tokenPublicScope == "" {
+		t.Skip("Env var GITHUB_TOKEN_PUBLIC missing")
+	}
+
+	type args struct {
+		owner   string
+		project string
+		token   string
+	}
+	tests := []struct {
+		name string
+		args args
+		want bool
+	}{
+		{
+			name: "public repository and token with scope 'public_repo",
+			args: args{
+				project: "git-bug",
+				owner:   "MichaelMure",
+				token:   tokenPublicScope,
+			},
+			want: true,
+		},
+		{
+			name: "private repository and token with scope 'repo",
+			args: args{
+				project: "git-bug-test-github-bridge",
+				owner:   "MichaelMure",
+				token:   tokenPrivateScope,
+			},
+			want: true,
+		},
+		{
+			name: "private repository and token with scope 'public_repo'",
+			args: args{
+				project: "git-bug-test-github-bridge",
+				owner:   "MichaelMure",
+				token:   tokenPublicScope,
+			},
+			want: false,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			ok, _ := validateProject(tt.args.owner, tt.args.project, tt.args.token)
+			assert.Equal(t, tt.want, ok)
+		})
+	}
+}

bridge/launchpad/config.go 🔗

@@ -7,12 +7,16 @@ import (
 	"os"
 	"regexp"
 	"strings"
+	"time"
 
 	"github.com/MichaelMure/git-bug/bridge/core"
 	"github.com/MichaelMure/git-bug/repository"
 )
 
-const keyProject = "project"
+const (
+	keyProject     = "project"
+	defaultTimeout = 60 * time.Second
+)
 
 var (
 	rxLaunchpadURL = regexp.MustCompile(`launchpad\.net[\/:]([^\/]*[a-z]+)`)
@@ -92,9 +96,10 @@ func promptProjectName() (string, error) {
 func validateProject(project string) (bool, error) {
 	url := fmt.Sprintf("%s/%s", apiRoot, project)
 
-	client := := &http.Client{
+	client := &http.Client{
 		Timeout: defaultTimeout,
 	}
+
 	resp, err := client.Get(url)
 	if err != nil {
 		return false, err