diff --git a/bitbucket.go b/bitbucket.go index 7fb5ff2..d874a6d 100644 --- a/bitbucket.go +++ b/bitbucket.go @@ -663,3 +663,10 @@ func (dk *DeployKeyOptions) WithContext(ctx context.Context) *DeployKeyOptions { dk.ctx = ctx return dk } + +type SSHKeyOptions struct { + Owner string `json:"owner"` + Uuid string `json:"uuid"` + Label string `json:"label"` + Key string `json:"key"` +} diff --git a/client.go b/client.go index cb0eae7..7ddc2d7 100644 --- a/client.go +++ b/client.go @@ -36,7 +36,7 @@ func apiBaseUrl() (*url.URL, error) { type Client struct { Auth *auth - Users users + Users *Users User user Teams teams Repositories *Repositories @@ -191,7 +191,10 @@ func injectClient(a *auth) *Client { Downloads: &Downloads{c: c}, DeployKeys: &DeployKeys{c: c}, } - c.Users = &Users{c: c} + c.Users = &Users{ + c: c, + SSHKeys: &SSHKeys{c: c}, + } c.User = &User{c: c} c.Teams = &Teams{c: c} c.Workspaces = &Workspace{c: c, Repositories: c.Repositories, Permissions: &Permission{c: c}} diff --git a/deploykeys.go b/deploykeys.go index 63fdd3c..2f795a5 100644 --- a/deploykeys.go +++ b/deploykeys.go @@ -2,6 +2,7 @@ package bitbucket import ( "encoding/json" + "errors" "github.com/mitchellh/mapstructure" ) @@ -17,6 +18,14 @@ type DeployKey struct { Comment string `json:"comment"` } +type DeployKeysRes struct { + Page int32 + Pagelen int32 + MaxDepth int32 + Size int32 + Items []DeployKey +} + func decodeDeployKey(response interface{}) (*DeployKey, error) { respMap := response.(map[string]interface{}) @@ -33,6 +42,50 @@ func decodeDeployKey(response interface{}) (*DeployKey, error) { return deployKey, nil } +func decodeDeployKeys(deployKeysResponse interface{}) (*DeployKeysRes, error) { + deployKeysResponseMap, ok := deployKeysResponse.(map[string]interface{}) + if !ok { + return nil, errors.New("not a valid format") + } + + repoArray := deployKeysResponseMap["values"].([]interface{}) + var deployKeys []DeployKey + for _, deployKeyEntry := range repoArray { + var deployKey DeployKey + err := mapstructure.Decode(deployKeyEntry, &deployKey) + if err == nil { + deployKeys = append(deployKeys, deployKey) + } + } + + page, ok := deployKeysResponseMap["page"].(float64) + if !ok { + page = 0 + } + + pagelen, ok := deployKeysResponseMap["pagelen"].(float64) + if !ok { + pagelen = 0 + } + maxDepth, ok := deployKeysResponseMap["max_width"].(float64) + if !ok { + maxDepth = 0 + } + size, ok := deployKeysResponseMap["size"].(float64) + if !ok { + size = 0 + } + + repositories := DeployKeysRes{ + Page: int32(page), + Pagelen: int32(pagelen), + MaxDepth: int32(maxDepth), + Size: int32(size), + Items: deployKeys, + } + return &repositories, nil +} + func buildDeployKeysBody(opt *DeployKeyOptions) (string, error) { body := map[string]interface{}{} body["label"] = opt.Label @@ -74,3 +127,13 @@ func (dk *DeployKeys) Delete(opt *DeployKeyOptions) (interface{}, error) { urlStr := dk.c.requestUrl("/repositories/%s/%s/deploy-keys/%d", opt.Owner, opt.RepoSlug, opt.Id) return dk.c.execute("DELETE", urlStr, "") } + +func (dk *DeployKeys) List(opt *DeployKeyOptions) (*DeployKeysRes, error) { + urlStr := dk.c.requestUrl("/repositories/%s/%s/deploy-keys", opt.Owner, opt.RepoSlug) + response, err := dk.c.execute("GET", urlStr, "") + if err != nil { + return nil, err + } + + return decodeDeployKeys(response) +} diff --git a/go.mod b/go.mod index 0aebde1..587eef8 100644 --- a/go.mod +++ b/go.mod @@ -6,8 +6,7 @@ require ( github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88 // indirect github.com/k0kubun/pp v3.0.1+incompatible github.com/kr/pretty v0.3.0 // indirect - github.com/mattn/go-colorable v0.0.9 // indirect - github.com/mattn/go-isatty v0.0.16 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect github.com/mitchellh/mapstructure v1.5.0 github.com/rogpeppe/go-internal v1.9.0 // indirect github.com/stretchr/testify v1.9.0 diff --git a/go.sum b/go.sum index b40d850..facac5e 100644 --- a/go.sum +++ b/go.sum @@ -17,8 +17,8 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/mattn/go-colorable v0.0.9 h1:UVL0vNpWh04HeJXV0KLcaT7r06gOH2l4OW6ddYRUIY4= -github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= diff --git a/repositories.go b/repositories.go index 751e3d6..ed02cbf 100644 --- a/repositories.go +++ b/repositories.go @@ -3,6 +3,7 @@ package bitbucket import ( "errors" "fmt" + "net/url" ) //"github.com/k0kubun/pp" @@ -35,16 +36,21 @@ func (r *Repositories) ListForAccount(ro *RepositoriesOptions) (*RepositoriesRes urlPath += fmt.Sprintf("/%s", ro.Owner) } urlStr := r.c.requestUrl(urlPath) + urlAsUrl, err := url.Parse(urlStr) + if err != nil { + return nil, err + } + q := urlAsUrl.Query() if ro.Role != "" { - urlStr += "?role=" + ro.Role + q.Set("role", ro.Role) } if ro.Keyword != nil && *ro.Keyword != "" { - if ro.Role == "" { - urlStr += "?" - } // https://developer.atlassian.com/cloud/bitbucket/rest/intro/#operators - urlStr += fmt.Sprintf("q=full_name ~ \"%s\"", *ro.Keyword) + query := fmt.Sprintf("full_name ~ \"%s\"", *ro.Keyword) + q.Set("q", query) } + urlAsUrl.RawQuery = q.Encode() + urlStr = urlAsUrl.String() repos, err := r.c.executePaginated("GET", urlStr, "", ro.Page) if err != nil { return nil, err diff --git a/ssh.go b/ssh.go new file mode 100644 index 0000000..a81b5c5 --- /dev/null +++ b/ssh.go @@ -0,0 +1,130 @@ +package bitbucket + +import ( + "encoding/json" + "errors" + + "github.com/mitchellh/mapstructure" +) + +type SSHKeys struct { + c *Client +} + +type SSHKey struct { + Uuid string `json:"uuid"` + Label string `json:"label"` + Key string `json:"key"` + Comment string `json:"comment"` + CreatedOm string `json:"created_on"` +} + +type SSHKeyRes struct { + Page int32 + Pagelen int32 + MaxDepth int32 + Size int32 + Items []SSHKey +} + +func decodeSSHKey(response interface{}) (*SSHKey, error) { + respMap := response.(map[string]interface{}) + + if respMap["type"] == "error" { + return nil, DecodeError(respMap) + } + + var sshKey = new(SSHKey) + err := mapstructure.Decode(respMap, sshKey) + if err != nil { + return nil, err + } + + return sshKey, nil +} + +func buildSSHKeysBody(opt *SSHKeyOptions) (string, error) { + body := map[string]interface{}{} + body["label"] = opt.Label + body["key"] = opt.Key + + data, err := json.Marshal(body) + if err != nil { + return "", err + } + + return string(data), nil +} + +func decodeSSHKeys(keysResponse interface{}) (*SSHKeyRes, error) { + keysResponseMap, ok := keysResponse.(map[string]interface{}) + if !ok { + return nil, errors.New("Not a valid format") + } + + keyArray := keysResponseMap["values"].([]interface{}) + var keys []SSHKey + for _, keyEntry := range keyArray { + var key SSHKey + err := mapstructure.Decode(keyEntry, &key) + if err == nil { + keys = append(keys, key) + } + } + + page, ok := keysResponseMap["page"].(float64) + if !ok { + page = 0 + } + + pagelen, ok := keysResponseMap["pagelen"].(float64) + if !ok { + pagelen = 0 + } + max_depth, ok := keysResponseMap["max_width"].(float64) + if !ok { + max_depth = 0 + } + size, ok := keysResponseMap["size"].(float64) + if !ok { + size = 0 + } + + keysResp := &SSHKeyRes{ + Page: int32(page), + Pagelen: int32(pagelen), + MaxDepth: int32(max_depth), + Size: int32(size), + Items: keys, + } + return keysResp, nil +} + +func (sk *SSHKeys) Create(ro *SSHKeyOptions) (*SSHKey, error) { + data, err := buildSSHKeysBody(ro) + if err != nil { + return nil, err + } + urlStr := sk.c.requestUrl("/users/%s/ssh-keys", ro.Owner) + response, err := sk.c.execute("POST", urlStr, data) + if err != nil { + return nil, err + } + + return decodeSSHKey(response) +} + +func (sk *SSHKeys) Get(ro *SSHKeyOptions) (*SSHKey, error) { + urlStr := sk.c.requestUrl("/users/%s/ssh-keys/%s", ro.Owner, ro.Uuid) + response, err := sk.c.execute("GET", urlStr, "") + if err != nil { + return nil, err + } + + return decodeSSHKey(response) +} + +func (sk *SSHKeys) Delete(ro *SSHKeyOptions) (interface{}, error) { + urlStr := sk.c.requestUrl("/users/%s/ssh-keys/%s", ro.Owner, ro.Uuid) + return sk.c.execute("DELETE", urlStr, "") +} diff --git a/tests/deploykeys_test.go b/tests/deploykeys_test.go index 604b1ce..7d18e6f 100644 --- a/tests/deploykeys_test.go +++ b/tests/deploykeys_test.go @@ -79,7 +79,7 @@ func TestDeployKey(t *testing.T) { } if deployKey.Id != deployKeyResourceId { - t.Error("The Deploy Key `label` attribute does not match the expected value.") + t.Error("The Deploy Key `id` attribute does not match the expected value.") } if deployKey.Label != label { t.Error("The Deploy Key `label` attribute does not match the expected value.") @@ -200,3 +200,132 @@ func TestDeployKeyWithComment(t *testing.T) { } }) } + +func TestListDeployKeys(t *testing.T) { + user := os.Getenv("BITBUCKET_TEST_USERNAME") + pass := os.Getenv("BITBUCKET_TEST_PASSWORD") + owner := os.Getenv("BITBUCKET_TEST_OWNER") + repo := os.Getenv("BITBUCKET_TEST_REPOSLUG") + + if user == "" { + t.Error("BITBUCKET_TEST_USERNAME is empty.") + } + if pass == "" { + t.Error("BITBUCKET_TEST_PASSWORD is empty.") + } + if owner == "" { + t.Error("BITBUCKET_TEST_OWNER is empty.") + } + if repo == "" { + t.Error("BITBUCKET_TEST_REPOSLUG is empty.") + } + + c := bitbucket.NewBasicAuth(user, pass) + + var deployKeyResourceId1 int + var deployKeyResourceId2 int + + t.Run("createKey1", func(t *testing.T) { + label := "go-test-1" + key := "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDAK/b1cHHDr/TEV1JGQl+WjCwStKG6Bhrv0rFpEsYlyTBm1fzN0VOJJYn4ZOPCPJwqse6fGbXntEs+BbXiptR+++HycVgl65TMR0b5ul5AgwrVdZdT7qjCOCgaSV74/9xlHDK8oqgGnfA7ZoBBU+qpVyaloSjBdJfLtPY/xqj4yHnXKYzrtn/uFc4Kp9Tb7PUg9Io3qohSTGJGVHnsVblq/rToJG7L5xIo0OxK0SJSQ5vuId93ZuFZrCNMXj8JDHZeSEtjJzpRCBEXHxpOPhAcbm4MzULgkFHhAVgp4JbkrT99/wpvZ7r9AdkTg7HGqL3rlaDrEcWfL7Lu6TnhBdq5" + opt := &bitbucket.DeployKeyOptions{ + Owner: owner, + RepoSlug: repo, + Label: label, + Key: key, + } + + deployKey, err := c.Repositories.DeployKeys.Create(opt) + if err != nil { + t.Error(err) + } + + if deployKey == nil { + t.Error("The Deploy Key could not be created.") + } + + if deployKey.Label != label { + t.Error("The Deploy Key `label` attribute does not match the expected value.") + } + if deployKey.Key != key { + t.Error("The Deploy Key `key` attribute does not match the expected value.") + } + + deployKeyResourceId1 = deployKey.Id + }) + + t.Run("createKey2", func(t *testing.T) { + label := "go-test-2" + key := "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDAK/b1cHHDr/TEV1JGQl+WjCwStKG6Bhrv0rFpEsYlyTBm1fzN0VOJJYn4ZOPCPJwqse6fGbXntEs+BbXiptR+++HycVgl65TMR0b5ul5AgwrVdZdT7qjCOCgaSV74/9xlHDK8oqgGnfA7ZoBBU+qpVyaloSjBdJfLtPY/xqj4yHnXKYzrtn/uFc4Kp9Tb7PUg9Io3qohSTGJGVHnsVblq/rToJG7L5xIo0OxK0SJSQ5vuId93ZuFZrCNMXj8JDHZeSEtjJzpRCBEXHxpOPhAcbm4MzULgkFHhAVgp4JbkrT99/wpvZ7r9AdkTg7HGqL3rlaDrEcWfL7Lu6TnhBdq6" + opt := &bitbucket.DeployKeyOptions{ + Owner: owner, + RepoSlug: repo, + Label: label, + Key: key, + } + + deployKey, err := c.Repositories.DeployKeys.Create(opt) + if err != nil { + t.Error(err) + } + + if deployKey == nil { + t.Error("The Deploy Key could not be created.") + } + + if deployKey.Label != label { + t.Error("The Deploy Key `label` attribute does not match the expected value.") + } + if deployKey.Key != key { + t.Error("The Deploy Key `key` attribute does not match the expected value.") + } + + deployKeyResourceId2 = deployKey.Id + }) + + t.Run("list", func(t *testing.T) { + opt := &bitbucket.DeployKeyOptions{ + Owner: owner, + RepoSlug: repo, + Id: deployKeyResourceId1, + } + deployKeys, err := c.Repositories.DeployKeys.List(opt) + if err != nil { + t.Error(err) + } + found := false + for _, r := range deployKeys.Items { + if r.Id == deployKeyResourceId2 { + found = true + break + } + } + if !found { + t.Error("Did not find deploy key in list") + } + + }) + // Clean up + t.Run("delete", func(t *testing.T) { + opt := &bitbucket.DeployKeyOptions{ + Owner: owner, + RepoSlug: repo, + Id: deployKeyResourceId1, + } + _, err := c.Repositories.DeployKeys.Delete(opt) + if err != nil { + t.Error(err) + } + }) + t.Run("delete", func(t *testing.T) { + opt := &bitbucket.DeployKeyOptions{ + Owner: owner, + RepoSlug: repo, + Id: deployKeyResourceId2, + } + _, err := c.Repositories.DeployKeys.Delete(opt) + if err != nil { + t.Error(err) + } + }) +} diff --git a/tests/repositories_test.go b/tests/repositories_test.go index 72f78fe..9cb7f34 100644 --- a/tests/repositories_test.go +++ b/tests/repositories_test.go @@ -47,6 +47,69 @@ func TestListForAccount(t *testing.T) { } } +func TestListForAccountWithKeyword(t *testing.T) { + user := os.Getenv("BITBUCKET_TEST_USERNAME") + pass := os.Getenv("BITBUCKET_TEST_PASSWORD") + owner := os.Getenv("BITBUCKET_TEST_OWNER") + repo := os.Getenv("BITBUCKET_TEST_REPOSLUG") + + if user == "" { + t.Error("BITBUCKET_TEST_USERNAME is empty.") + } + if pass == "" { + t.Error("BITBUCKET_TEST_PASSWORD is empty.") + } + if owner == "" { + t.Error("BITBUCKET_TEST_OWNER is empty.") + } + if repo == "" { + t.Error("BITBUCKET_TEST_REPOSLUG is empty.") + } + + c := bitbucket.NewBasicAuth(user, pass) + t.Run("only keyword", func(t *testing.T) { + repositories, err := c.Repositories.ListForAccount(&bitbucket.RepositoriesOptions{ + Owner: owner, + Keyword: &repo, + }) + if err != nil { + t.Error("Unable to fetch repositories") + } + + found := false + for _, r := range repositories.Items { + if r.Slug == repo { + found = true + break + } + } + if !found { + t.Error("Did not find repository in list") + } + }) + t.Run("keyword and role", func(t *testing.T) { + repositories, err := c.Repositories.ListForAccount(&bitbucket.RepositoriesOptions{ + Owner: owner, + Keyword: &repo, + Role: "member", + }) + if err != nil { + t.Error("Unable to fetch repositories") + } + + found := false + for _, r := range repositories.Items { + if r.Slug == repo { + found = true + break + } + } + if !found { + t.Error("Did not find repository in list") + } + }) +} + func TestListForTeam(t *testing.T) { user := os.Getenv("BITBUCKET_TEST_USERNAME") pass := os.Getenv("BITBUCKET_TEST_PASSWORD") diff --git a/tests/ssh_test.go b/tests/ssh_test.go new file mode 100644 index 0000000..6f22472 --- /dev/null +++ b/tests/ssh_test.go @@ -0,0 +1,79 @@ +package tests + +import ( + "os" + "testing" + + "github.com/ktrysmt/go-bitbucket" +) + +func TestUserSSHKey(t *testing.T) { + user := os.Getenv("BITBUCKET_TEST_USERNAME") + pass := os.Getenv("BITBUCKET_TEST_PASSWORD") + owner := os.Getenv("BITBUCKET_TEST_OWNER") + + if user == "" { + t.Error("BITBUCKET_TEST_USERNAME is empty.") + } + if pass == "" { + t.Error("BITBUCKET_TEST_PASSWORD is empty.") + } + if owner == "" { + t.Error("BITBUCKET_TEST_OWNER is empty.") + } + + c := bitbucket.NewBasicAuth(user, pass) + + var sshKeyResourceUuid string + + label := "go-user-test" + key := "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCjuh+EUAXrLtlQ5LfiSf4nWVOAuWUwMy+Cb+AkqFolyw/tuZh0tx9cEzSHddWgeFSJa5Zj0OYUEVTMkhUUfvKb8tfqbTGr0EWZrW6Odc6bqQXBDa48IfSqPYHmmdJh07MpRRQRqEMHB4WfnNuEUhOuNHr2lOX7BtCyp4r38gkuNBFmT6nheSoxSjJ6t3VbViyO+p2RY1RaGL77kUMgt4ti4MR4lNuUBT+BOxiILHqwWfY0z0i7Cc1zW4PvDbFtgHzSzQBdBel3vjk5AALZV31tiu0R21Gxm35n5L2N12ZgTXVXOVC1qfGzh6OR+7ZG0/iWyCmOoi+cOznXlnQEC/k5" + t.Run("create", func(t *testing.T) { + keyOptions := &bitbucket.SSHKeyOptions{ + Label: label, + Key: key, + Owner: user, + } + sshUserKey, err := c.Users.SSHKeys.Create(keyOptions) + if err != nil { + t.Error(err) + } + if sshUserKey == nil { + t.Error("The User SSH Key could not be created.") + } + sshKeyResourceUuid = sshUserKey.Uuid + }) + t.Run("get", func(t *testing.T) { + keyOptions := &bitbucket.SSHKeyOptions{ + Owner: user, + Uuid: sshKeyResourceUuid, + } + sshKey, err := c.Users.SSHKeys.Get(keyOptions) + if err != nil { + t.Error(err) + } + if sshKey == nil { + t.Error("The Deploy Key could not be retrieved.") + } + if sshKey.Uuid != sshKeyResourceUuid { + t.Error("The SSH Key `id` attribute does not match the expected value.") + } + if sshKey.Label != label { + t.Error("The SSH Key `label` attribute does not match the expected value.") + } + if sshKey.Key != key { + t.Error("The SSH Key `key` attribute does not match the expected value.") + } + }) + + t.Run("delete", func(t *testing.T) { + keyOptions := &bitbucket.SSHKeyOptions{ + Owner: user, + Uuid: sshKeyResourceUuid, + } + _, err := c.Users.SSHKeys.Delete(keyOptions) + if err != nil { + t.Error(err) + } + }) +} diff --git a/users.go b/users.go index 73cae22..51b41c2 100644 --- a/users.go +++ b/users.go @@ -1,7 +1,9 @@ package bitbucket type Users struct { - c *Client + c *Client + SSHKeys *SSHKeys + users } func (u *Users) Get(t string) (*User, error) {