increased coverage for bitbucket package

This commit is contained in:
Brad Rydzewski 2016-04-30 01:00:39 -07:00
parent 082570fb5b
commit 7c5257b61e
13 changed files with 1103 additions and 327 deletions

View file

@ -1,9 +1,7 @@
package bitbucket
import (
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"net/url"
@ -16,7 +14,11 @@ import (
"golang.org/x/oauth2/bitbucket"
)
// Bitbucket Server endpoint.
const Endpoint = "https://api.bitbucket.org"
type config struct {
URL string
Client string
Secret string
}
@ -25,6 +27,7 @@ type config struct {
// repository hosting service at https://bitbucket.org
func New(client, secret string) remote.Remote {
return &config{
URL: Endpoint,
Client: client,
Secret: secret,
}
@ -33,6 +36,7 @@ func New(client, secret string) remote.Remote {
// helper function to return the bitbucket oauth2 client
func (c *config) newClient(u *model.User) *internal.Client {
return internal.NewClientToken(
c.URL,
c.Client,
c.Secret,
&oauth2.Token{
@ -61,7 +65,7 @@ func (c *config) Login(res http.ResponseWriter, req *http.Request) (*model.User,
return nil, err
}
client := internal.NewClient(config.Client(oauth2.NoContext, token))
client := internal.NewClient(c.URL, config.Client(oauth2.NoContext, token))
curr, err := client.FindCurrent()
if err != nil {
return nil, err
@ -72,6 +76,7 @@ func (c *config) Login(res http.ResponseWriter, req *http.Request) (*model.User,
func (c *config) Auth(token, secret string) (string, error) {
client := internal.NewClientToken(
c.URL,
c.Client,
c.Secret,
&oauth2.Token{
@ -196,8 +201,8 @@ func (c *config) File(u *model.User, r *model.Repo, b *model.Build, f string) ([
func (c *config) Status(u *model.User, r *model.Repo, b *model.Build, link string) error {
status := internal.BuildStatus{
State: getStatus(b.Status),
Desc: getDesc(b.Status),
State: convertStatus(b.Status),
Desc: convertDesc(b.Status),
Key: "Drone",
Url: link,
}
@ -219,10 +224,7 @@ func (c *config) Activate(u *model.User, r *model.Repo, k *model.Key, link strin
}
// deletes any previously created hooks
if err := c.Deactivate(u, r, link); err != nil {
// we can live with failure here. Things happen and manually scrubbing
// hooks is certinaly not the end of the world.
}
c.Deactivate(u, r, link)
return c.newClient(u).CreateHook(r.Owner, r.Name, &internal.Hook{
Active: true,
@ -242,114 +244,22 @@ func (c *config) Deactivate(u *model.User, r *model.Repo, link string) error {
hooks, err := client.ListHooks(r.Owner, r.Name, &internal.ListOpts{})
if err != nil {
return nil // we can live with undeleted hooks
return err
}
for _, hook := range hooks.Values {
hookurl, err := url.Parse(hook.Url)
if err != nil {
continue
}
if hookurl.Host == linkurl.Host {
client.DeleteHook(r.Owner, r.Name, hook.Uuid)
break // we can live with undeleted hooks
if err == nil && hookurl.Host == linkurl.Host {
return client.DeleteHook(r.Owner, r.Name, hook.Uuid)
}
}
return nil
}
// Hook parses the incoming Bitbucket hook and returns the Repository and
// Build details. If the hook is unsupported nil values are returned and the
// hook should be skipped.
func (c *config) Hook(r *http.Request) (*model.Repo, *model.Build, error) {
switch r.Header.Get("X-Event-Key") {
case "repo:push":
return c.pushHook(r)
case "pullrequest:created", "pullrequest:updated":
return c.pullHook(r)
}
return nil, nil, nil
}
func (c *config) pushHook(r *http.Request) (*model.Repo, *model.Build, error) {
payload := []byte(r.FormValue("payload"))
if len(payload) == 0 {
defer r.Body.Close()
payload, _ = ioutil.ReadAll(r.Body)
}
hook := internal.PushHook{}
err := json.Unmarshal(payload, &hook)
if err != nil {
return nil, nil, err
}
// the hook can container one or many changes. Since I don't
// fully understand this yet, we will just pick the first
// change that has branch information.
for _, change := range hook.Push.Changes {
// must have sha information
if change.New.Target.Hash == "" {
continue
}
// we only support tag and branch pushes for now
buildEventType := model.EventPush
buildRef := fmt.Sprintf("refs/heads/%s", change.New.Name)
if change.New.Type == "tag" || change.New.Type == "annotated_tag" || change.New.Type == "bookmark" {
buildEventType = model.EventTag
buildRef = fmt.Sprintf("refs/tags/%s", change.New.Name)
} else if change.New.Type != "branch" && change.New.Type != "named_branch" {
continue
}
// return the updated repository information and the
// build information.
// TODO(bradrydzewski) uses unit tested conversion function
return convertRepo(&hook.Repo), &model.Build{
Event: buildEventType,
Commit: change.New.Target.Hash,
Ref: buildRef,
Link: change.New.Target.Links.Html.Href,
Branch: change.New.Name,
Message: change.New.Target.Message,
Avatar: hook.Actor.Links.Avatar.Href,
Author: hook.Actor.Login,
Timestamp: change.New.Target.Date.UTC().Unix(),
}, nil
}
return nil, nil, nil
}
func (c *config) pullHook(r *http.Request) (*model.Repo, *model.Build, error) {
payload := []byte(r.FormValue("payload"))
if len(payload) == 0 {
defer r.Body.Close()
payload, _ = ioutil.ReadAll(r.Body)
}
hook := internal.PullRequestHook{}
err := json.Unmarshal(payload, &hook)
if err != nil {
return nil, nil, err
}
if hook.PullRequest.State != "OPEN" {
return nil, nil, nil
}
// TODO(bradrydzewski) uses unit tested conversion function
return convertRepo(&hook.Repo), &model.Build{
Event: model.EventPull,
Commit: hook.PullRequest.Dest.Commit.Hash,
Ref: fmt.Sprintf("refs/heads/%s", hook.PullRequest.Dest.Branch.Name),
Refspec: fmt.Sprintf("https://bitbucket.org/%s.git", hook.PullRequest.Source.Repo.FullName),
Remote: cloneLink(&hook.PullRequest.Dest.Repo),
Link: hook.PullRequest.Links.Html.Href,
Branch: hook.PullRequest.Dest.Branch.Name,
Message: hook.PullRequest.Desc,
Avatar: hook.Actor.Links.Avatar.Href,
Author: hook.Actor.Login,
Timestamp: hook.PullRequest.Updated.UTC().Unix(),
}, nil
return parseHook(r)
}

View file

@ -0,0 +1,253 @@
package bitbucket
import (
"bytes"
"net/http"
"net/http/httptest"
"testing"
"github.com/drone/drone/model"
"github.com/drone/drone/remote/bitbucket/fixtures"
"github.com/franela/goblin"
"github.com/gin-gonic/gin"
)
func Test_bitbucket(t *testing.T) {
gin.SetMode(gin.TestMode)
s := httptest.NewServer(fixtures.Handler())
c := &config{URL: s.URL}
g := goblin.Goblin(t)
g.Describe("Bitbucket client", func() {
g.After(func() {
s.Close()
})
g.It("Should return client with default endpoint", func() {
remote := New("4vyW6b49Z", "a5012f6c6")
g.Assert(remote.(*config).URL).Equal("https://api.bitbucket.org")
g.Assert(remote.(*config).Client).Equal("4vyW6b49Z")
g.Assert(remote.(*config).Secret).Equal("a5012f6c6")
})
g.It("Should return the netrc file", func() {
remote := New("", "")
netrc, _ := remote.Netrc(fakeUser, nil)
g.Assert(netrc.Machine).Equal("bitbucket.org")
g.Assert(netrc.Login).Equal("x-token-auth")
g.Assert(netrc.Password).Equal(fakeUser.Token)
})
g.Describe("Given an access token", func() {
g.It("Should return the authenticated user", func() {
login, err := c.Auth(
fakeUser.Token,
fakeUser.Secret,
)
g.Assert(err == nil).IsTrue()
g.Assert(login).Equal(fakeUser.Login)
})
g.It("Should return error when request fails", func() {
_, err := c.Auth(
fakeUserNotFound.Token,
fakeUserNotFound.Secret,
)
g.Assert(err != nil).IsTrue()
})
})
g.Describe("When requesting a repository", func() {
g.It("Should return the details", func() {
repo, err := c.Repo(
fakeUser,
fakeRepo.Owner,
fakeRepo.Name,
)
g.Assert(err == nil).IsTrue()
g.Assert(repo.FullName).Equal(fakeRepo.FullName)
})
g.It("Should handle not found errors", func() {
_, err := c.Repo(
fakeUser,
fakeRepoNotFound.Owner,
fakeRepoNotFound.Name,
)
g.Assert(err != nil).IsTrue()
})
})
g.Describe("When requesting repository permissions", func() {
g.It("Should handle not found errors", func() {
_, err := c.Perm(
fakeUser,
fakeRepoNotFound.Owner,
fakeRepoNotFound.Name,
)
g.Assert(err != nil).IsTrue()
})
g.It("Should authorize read access", func() {
perm, err := c.Perm(
fakeUser,
fakeRepoNoHooks.Owner,
fakeRepoNoHooks.Name,
)
g.Assert(err == nil).IsTrue()
g.Assert(perm.Pull).IsTrue()
g.Assert(perm.Push).IsFalse()
g.Assert(perm.Admin).IsFalse()
})
g.It("Should authorize admin access", func() {
perm, err := c.Perm(
fakeUser,
fakeRepo.Owner,
fakeRepo.Name,
)
g.Assert(err == nil).IsTrue()
g.Assert(perm.Pull).IsTrue()
g.Assert(perm.Push).IsTrue()
g.Assert(perm.Admin).IsTrue()
})
})
g.Describe("When requesting user repositories", func() {
g.It("Should return the details", func() {
repos, err := c.Repos(fakeUser)
g.Assert(err == nil).IsTrue()
g.Assert(repos[0].FullName).Equal(fakeRepo.FullName)
})
g.It("Should handle organization not found errors", func() {
_, err := c.Repos(fakeUserNoTeams)
g.Assert(err != nil).IsTrue()
})
g.It("Should handle not found errors", func() {
_, err := c.Repos(fakeUserNoRepos)
g.Assert(err != nil).IsTrue()
})
})
g.Describe("When requesting user teams", func() {
g.It("Should return the details", func() {
teams, err := c.Teams(fakeUser)
g.Assert(err == nil).IsTrue()
g.Assert(teams[0].Login).Equal("superfriends")
g.Assert(teams[0].Avatar).Equal("http://i.imgur.com/ZygP55A.jpg")
})
g.It("Should handle not found error", func() {
_, err := c.Teams(fakeUserNoTeams)
g.Assert(err != nil).IsTrue()
})
})
g.Describe("When downloading a file", func() {
g.It("Should return the bytes", func() {
raw, err := c.File(fakeUser, fakeRepo, fakeBuild, "file")
g.Assert(err == nil).IsTrue()
g.Assert(len(raw) != 0).IsTrue()
})
g.It("Should handle not found error", func() {
_, err := c.File(fakeUser, fakeRepo, fakeBuild, "file_not_found")
g.Assert(err != nil).IsTrue()
})
})
g.Describe("When activating a repository", func() {
g.It("Should error when malformed hook", func() {
err := c.Activate(fakeUser, fakeRepo, nil, "%gh&%ij")
g.Assert(err != nil).IsTrue()
})
g.It("Should create the hook", func() {
err := c.Activate(fakeUser, fakeRepo, nil, "http://127.0.0.1")
g.Assert(err == nil).IsTrue()
})
g.It("Should remove previous hooks")
})
g.Describe("When deactivating a repository", func() {
g.It("Should error when malformed hook", func() {
err := c.Deactivate(fakeUser, fakeRepo, "%gh&%ij")
g.Assert(err != nil).IsTrue()
})
g.It("Should error when listing hooks fails", func() {
err := c.Deactivate(fakeUser, fakeRepoNoHooks, "http://127.0.0.1")
g.Assert(err != nil).IsTrue()
})
g.It("Should successfully remove hooks", func() {
err := c.Deactivate(fakeUser, fakeRepo, "http://127.0.0.1")
g.Assert(err == nil).IsTrue()
})
g.It("Should successfully deactivate when hook already removed", func() {
err := c.Deactivate(fakeUser, fakeRepoEmptyHook, "http://127.0.0.1")
g.Assert(err == nil).IsTrue()
})
})
g.It("Should update the status", func() {
err := c.Status(fakeUser, fakeRepo, fakeBuild, "http://127.0.0.1")
g.Assert(err == nil).IsTrue()
})
g.It("Should parse the hook", func() {
buf := bytes.NewBufferString(fixtures.HookPush)
req, _ := http.NewRequest("POST", "/hook", buf)
req.Header = http.Header{}
req.Header.Set(hookEvent, hookPush)
r, _, err := c.Hook(req)
g.Assert(err == nil).IsTrue()
g.Assert(r.FullName).Equal("user_name/repo_name")
})
})
}
var (
fakeUser = &model.User{
Login: "superman",
Token: "cfcd2084",
}
fakeUserNotFound = &model.User{
Login: "superman",
Token: "user_not_found",
}
fakeUserNoTeams = &model.User{
Login: "superman",
Token: "teams_not_found",
}
fakeUserNoRepos = &model.User{
Login: "superman",
Token: "repos_not_found",
}
fakeRepo = &model.Repo{
Owner: "test_name",
Name: "repo_name",
FullName: "test_name/repo_name",
}
fakeRepoNotFound = &model.Repo{
Owner: "test_name",
Name: "repo_not_found",
FullName: "test_name/repo_not_found",
}
fakeRepoNoHooks = &model.Repo{
Owner: "test_name",
Name: "hooks_not_found",
FullName: "test_name/hooks_not_found",
}
fakeRepoEmptyHook = &model.Repo{
Owner: "test_name",
Name: "hook_empty",
FullName: "test_name/hook_empty",
}
fakeBuild = &model.Build{
Commit: "9ecad50",
}
)

View file

@ -1,40 +0,0 @@
package bitbucket
import "github.com/drone/drone/model"
const (
statusPending = "INPROGRESS"
statusSuccess = "SUCCESSFUL"
statusFailure = "FAILED"
)
const (
descPending = "this build is pending"
descSuccess = "the build was successful"
descFailure = "the build failed"
descError = "oops, something went wrong"
)
func getStatus(status string) string {
switch status {
case model.StatusPending, model.StatusRunning:
return statusPending
case model.StatusSuccess:
return statusSuccess
default:
return statusFailure
}
}
func getDesc(status string) string {
switch status {
case model.StatusPending, model.StatusRunning:
return descPending
case model.StatusSuccess:
return descSuccess
case model.StatusFailure:
return descFailure
default:
return descError
}
}

View file

@ -1,43 +0,0 @@
package bitbucket
import (
"testing"
"github.com/drone/drone/model"
"github.com/franela/goblin"
)
func Test_status(t *testing.T) {
g := goblin.Goblin(t)
g.Describe("Bitbucket status", func() {
g.It("should return passing", func() {
g.Assert(getStatus(model.StatusSuccess)).Equal(statusSuccess)
})
g.It("should return pending", func() {
g.Assert(getStatus(model.StatusPending)).Equal(statusPending)
g.Assert(getStatus(model.StatusRunning)).Equal(statusPending)
})
g.It("should return failing", func() {
g.Assert(getStatus(model.StatusFailure)).Equal(statusFailure)
g.Assert(getStatus(model.StatusKilled)).Equal(statusFailure)
g.Assert(getStatus(model.StatusError)).Equal(statusFailure)
})
g.It("should return passing desc", func() {
g.Assert(getDesc(model.StatusSuccess)).Equal(descSuccess)
})
g.It("should return pending desc", func() {
g.Assert(getDesc(model.StatusPending)).Equal(descPending)
g.Assert(getDesc(model.StatusRunning)).Equal(descPending)
})
g.It("should return failing desc", func() {
g.Assert(getDesc(model.StatusFailure)).Equal(descFailure)
})
g.It("should return error desc", func() {
g.Assert(getDesc(model.StatusKilled)).Equal(descError)
g.Assert(getDesc(model.StatusError)).Equal(descError)
})
})
}

View file

@ -1,6 +1,7 @@
package bitbucket
import (
"fmt"
"net/url"
"strings"
@ -10,6 +11,47 @@ import (
"golang.org/x/oauth2"
)
const (
statusPending = "INPROGRESS"
statusSuccess = "SUCCESSFUL"
statusFailure = "FAILED"
)
const (
descPending = "this build is pending"
descSuccess = "the build was successful"
descFailure = "the build failed"
descError = "oops, something went wrong"
)
// convertStatus is a helper function used to convert a Drone status to a
// Bitbucket commit status.
func convertStatus(status string) string {
switch status {
case model.StatusPending, model.StatusRunning:
return statusPending
case model.StatusSuccess:
return statusSuccess
default:
return statusFailure
}
}
// convertDesc is a helper function used to convert a Drone status to a
// Bitbucket status description.
func convertDesc(status string) string {
switch status {
case model.StatusPending, model.StatusRunning:
return descPending
case model.StatusSuccess:
return descSuccess
case model.StatusFailure:
return descFailure
default:
return descError
}
}
// convertRepo is a helper function used to convert a Bitbucket repository
// structure to the common Drone repository structure.
func convertRepo(from *internal.Repo) *model.Repo {
@ -102,3 +144,43 @@ func convertTeam(from *internal.Account) *model.Team {
Avatar: from.Links.Avatar.Href,
}
}
// convertPullHook is a helper function used to convert a Bitbucket pull request
// hook to the Drone build struct holding commit information.
func convertPullHook(from *internal.PullRequestHook) *model.Build {
return &model.Build{
Event: model.EventPull,
Commit: from.PullRequest.Dest.Commit.Hash,
Ref: fmt.Sprintf("refs/heads/%s", from.PullRequest.Dest.Branch.Name),
Remote: cloneLink(&from.PullRequest.Dest.Repo),
Link: from.PullRequest.Links.Html.Href,
Branch: from.PullRequest.Dest.Branch.Name,
Message: from.PullRequest.Desc,
Avatar: from.Actor.Links.Avatar.Href,
Author: from.Actor.Login,
Timestamp: from.PullRequest.Updated.UTC().Unix(),
}
}
// convertPushHook is a helper function used to convert a Bitbucket push
// hook to the Drone build struct holding commit information.
func convertPushHook(hook *internal.PushHook, change *internal.Change) *model.Build {
build := &model.Build{
Commit: change.New.Target.Hash,
Link: change.New.Target.Links.Html.Href,
Branch: change.New.Name,
Message: change.New.Target.Message,
Avatar: hook.Actor.Links.Avatar.Href,
Author: hook.Actor.Login,
Timestamp: change.New.Target.Date.UTC().Unix(),
}
switch change.New.Type {
case "tag", "annotated_tag", "bookmark":
build.Event = model.EventTag
build.Ref = fmt.Sprintf("refs/tags/%s", change.New.Name)
default:
build.Event = model.EventPush
build.Ref = fmt.Sprintf("refs/heads/%s", change.New.Name)
}
return build
}

View file

@ -0,0 +1,195 @@
package bitbucket
import (
"testing"
"time"
"github.com/drone/drone/model"
"github.com/drone/drone/remote/bitbucket/internal"
"github.com/franela/goblin"
"golang.org/x/oauth2"
)
func Test_helper(t *testing.T) {
g := goblin.Goblin(t)
g.Describe("Bitbucket converter", func() {
g.It("should convert passing status", func() {
g.Assert(convertStatus(model.StatusSuccess)).Equal(statusSuccess)
})
g.It("should convert pending status", func() {
g.Assert(convertStatus(model.StatusPending)).Equal(statusPending)
g.Assert(convertStatus(model.StatusRunning)).Equal(statusPending)
})
g.It("should convert failing status", func() {
g.Assert(convertStatus(model.StatusFailure)).Equal(statusFailure)
g.Assert(convertStatus(model.StatusKilled)).Equal(statusFailure)
g.Assert(convertStatus(model.StatusError)).Equal(statusFailure)
})
g.It("should convert passing desc", func() {
g.Assert(convertDesc(model.StatusSuccess)).Equal(descSuccess)
})
g.It("should convert pending desc", func() {
g.Assert(convertDesc(model.StatusPending)).Equal(descPending)
g.Assert(convertDesc(model.StatusRunning)).Equal(descPending)
})
g.It("should convert failing desc", func() {
g.Assert(convertDesc(model.StatusFailure)).Equal(descFailure)
})
g.It("should convert error desc", func() {
g.Assert(convertDesc(model.StatusKilled)).Equal(descError)
g.Assert(convertDesc(model.StatusError)).Equal(descError)
})
g.It("should convert repository lite", func() {
from := &internal.Repo{}
from.FullName = "octocat/hello-world"
from.Owner.Links.Avatar.Href = "http://..."
to := convertRepoLite(from)
g.Assert(to.Avatar).Equal(from.Owner.Links.Avatar.Href)
g.Assert(to.FullName).Equal(from.FullName)
g.Assert(to.Owner).Equal("octocat")
g.Assert(to.Name).Equal("hello-world")
})
g.It("should convert repository", func() {
from := &internal.Repo{
FullName: "octocat/hello-world",
IsPrivate: true,
Scm: "hg",
}
from.Owner.Links.Avatar.Href = "http://..."
from.Links.Html.Href = "https://bitbucket.org/foo/bar"
to := convertRepo(from)
g.Assert(to.Avatar).Equal(from.Owner.Links.Avatar.Href)
g.Assert(to.FullName).Equal(from.FullName)
g.Assert(to.Owner).Equal("octocat")
g.Assert(to.Name).Equal("hello-world")
g.Assert(to.Branch).Equal("default")
g.Assert(to.Kind).Equal(from.Scm)
g.Assert(to.IsPrivate).Equal(from.IsPrivate)
g.Assert(to.Clone).Equal(from.Links.Html.Href)
g.Assert(to.Link).Equal(from.Links.Html.Href)
})
g.It("should convert team", func() {
from := &internal.Account{Login: "octocat"}
from.Links.Avatar.Href = "http://..."
to := convertTeam(from)
g.Assert(to.Avatar).Equal(from.Links.Avatar.Href)
g.Assert(to.Login).Equal(from.Login)
})
g.It("should convert team list", func() {
from := &internal.Account{Login: "octocat"}
from.Links.Avatar.Href = "http://..."
to := convertTeamList([]*internal.Account{from})
g.Assert(to[0].Avatar).Equal(from.Links.Avatar.Href)
g.Assert(to[0].Login).Equal(from.Login)
})
g.It("should convert user", func() {
token := &oauth2.Token{
AccessToken: "foo",
RefreshToken: "bar",
Expiry: time.Now(),
}
user := &internal.Account{Login: "octocat"}
user.Links.Avatar.Href = "http://..."
result := convertUser(user, token)
g.Assert(result.Avatar).Equal(user.Links.Avatar.Href)
g.Assert(result.Login).Equal(user.Login)
g.Assert(result.Token).Equal(token.AccessToken)
g.Assert(result.Token).Equal(token.AccessToken)
g.Assert(result.Secret).Equal(token.RefreshToken)
g.Assert(result.Expiry).Equal(token.Expiry.UTC().Unix())
})
g.It("should use clone url", func() {
repo := &internal.Repo{}
repo.Links.Clone = append(repo.Links.Clone, internal.Link{
Name: "https",
Href: "https://bitbucket.org/foo/bar.git",
})
link := cloneLink(repo)
g.Assert(link).Equal(repo.Links.Clone[0].Href)
})
g.It("should build clone url", func() {
repo := &internal.Repo{}
repo.Links.Html.Href = "https://foo:bar@bitbucket.org/foo/bar.git"
link := cloneLink(repo)
g.Assert(link).Equal("https://bitbucket.org/foo/bar.git")
})
g.It("should convert pull hook to build", func() {
hook := &internal.PullRequestHook{}
hook.Actor.Login = "octocat"
hook.Actor.Links.Avatar.Href = "https://..."
hook.PullRequest.Dest.Commit.Hash = "73f9c44d"
hook.PullRequest.Dest.Branch.Name = "master"
hook.PullRequest.Dest.Repo.Links.Html.Href = "https://bitbucket.org/foo/bar"
hook.PullRequest.Links.Html.Href = "https://bitbucket.org/foo/bar/pulls/5"
hook.PullRequest.Desc = "updated README"
hook.PullRequest.Updated = time.Now()
build := convertPullHook(hook)
g.Assert(build.Event).Equal(model.EventPull)
g.Assert(build.Author).Equal(hook.Actor.Login)
g.Assert(build.Avatar).Equal(hook.Actor.Links.Avatar.Href)
g.Assert(build.Commit).Equal(hook.PullRequest.Dest.Commit.Hash)
g.Assert(build.Branch).Equal(hook.PullRequest.Dest.Branch.Name)
g.Assert(build.Link).Equal(hook.PullRequest.Links.Html.Href)
g.Assert(build.Ref).Equal("refs/heads/master")
g.Assert(build.Message).Equal(hook.PullRequest.Desc)
g.Assert(build.Timestamp).Equal(hook.PullRequest.Updated.Unix())
})
g.It("should convert push hook to build", func() {
change := internal.Change{}
change.New.Target.Hash = "73f9c44d"
change.New.Name = "master"
change.New.Target.Links.Html.Href = "https://bitbucket.org/foo/bar/commits/73f9c44d"
change.New.Target.Message = "updated README"
change.New.Target.Date = time.Now()
hook := internal.PushHook{}
hook.Actor.Login = "octocat"
hook.Actor.Links.Avatar.Href = "https://..."
build := convertPushHook(&hook, &change)
g.Assert(build.Event).Equal(model.EventPush)
g.Assert(build.Author).Equal(hook.Actor.Login)
g.Assert(build.Avatar).Equal(hook.Actor.Links.Avatar.Href)
g.Assert(build.Commit).Equal(change.New.Target.Hash)
g.Assert(build.Branch).Equal(change.New.Name)
g.Assert(build.Link).Equal(change.New.Target.Links.Html.Href)
g.Assert(build.Ref).Equal("refs/heads/master")
g.Assert(build.Message).Equal(change.New.Target.Message)
g.Assert(build.Timestamp).Equal(change.New.Target.Date.Unix())
})
g.It("should convert tag hook to build", func() {
change := internal.Change{}
change.New.Name = "v1.0.0"
change.New.Type = "tag"
hook := internal.PushHook{}
build := convertPushHook(&hook, &change)
g.Assert(build.Event).Equal(model.EventTag)
g.Assert(build.Ref).Equal("refs/tags/v1.0.0")
})
})
}

View file

@ -0,0 +1,181 @@
package fixtures
import (
"net/http"
"github.com/gin-gonic/gin"
)
// Handler returns an http.Handler that is capable of handling a variety of mock
// Bitbucket requests and returning mock responses.
func Handler() http.Handler {
gin.SetMode(gin.TestMode)
e := gin.New()
e.GET("/2.0/repositories/:owner/:name", getRepo)
e.GET("/2.0/repositories/:owner/:name/hooks", getRepoHooks)
e.GET("/1.0/repositories/:owner/:name/src/:commit/:file", getRepoFile)
e.DELETE("/2.0/repositories/:owner/:name/hooks/:hook", deleteRepoHook)
e.POST("/2.0/repositories/:owner/:name/hooks", createRepoHook)
e.POST("/2.0/repositories/:owner/:name/commit/:commit/statuses/build", createRepoStatus)
e.GET("/2.0/repositories/:owner", getUserRepos)
e.GET("/2.0/teams/", getUserTeams)
e.GET("/2.0/user/", getUser)
return e
}
func getRepo(c *gin.Context) {
switch c.Param("name") {
case "not_found", "repo_unknown", "repo_not_found":
c.String(404, "")
default:
c.String(200, repoPayload)
}
}
func getRepoHooks(c *gin.Context) {
switch c.Param("name") {
case "hooks_not_found", "repo_no_hooks":
c.String(404, "")
case "hook_empty":
c.String(200, "{}")
default:
c.String(200, repoHookPayload)
}
}
func getRepoFile(c *gin.Context) {
switch c.Param("file") {
case "file_not_found":
c.String(404, "")
default:
c.String(200, repoFilePayload)
}
}
func createRepoStatus(c *gin.Context) {
switch c.Param("name") {
case "repo_not_found":
c.String(404, "")
default:
c.String(200, "")
}
}
func createRepoHook(c *gin.Context) {
c.String(200, "")
}
func deleteRepoHook(c *gin.Context) {
switch c.Param("name") {
case "hook_not_found":
c.String(404, "")
default:
c.String(200, "")
}
}
func getUser(c *gin.Context) {
switch c.Request.Header.Get("Authorization") {
case "Bearer user_not_found", "Bearer a87ff679":
c.String(404, "")
default:
c.String(200, userPayload)
}
}
func getUserTeams(c *gin.Context) {
switch c.Request.Header.Get("Authorization") {
case "Bearer teams_not_found", "Bearer c81e728d":
c.String(404, "")
default:
c.String(200, userTeamPayload)
}
}
func getUserRepos(c *gin.Context) {
switch c.Request.Header.Get("Authorization") {
case "Bearer repos_not_found", "Bearer 70efdf2e":
c.String(404, "")
default:
c.String(200, userRepoPayload)
}
}
const repoPayload = `
{
"full_name": "test_name/repo_name",
"scm": "git",
"is_private": true
}
`
const repoHookPayload = `
{
"pagelen": 10,
"values": [
{
"uuid": "{afe61e14-2c5f-49e8-8b68-ad1fb55fc052}",
"url": "http://127.0.0.1"
}
],
"page": 1,
"size": 1
}
`
const repoFilePayload = `
{
"data": "{ platform: linux/amd64 }"
}
`
const userPayload = `
{
"username": "superman",
"links": {
"avatar": {
"href": "http:\/\/i.imgur.com\/ZygP55A.jpg"
}
},
"type": "user"
}
`
const userRepoPayload = `
{
"page": 1,
"pagelen": 10,
"size": 1,
"values": [
{
"links": {
"avatar": {
"href": "http:\/\/i.imgur.com\/ZygP55A.jpg"
}
},
"full_name": "test_name/repo_name",
"scm": "git",
"is_private": true
}
]
}
`
const userTeamPayload = `
{
"pagelen": 100,
"values": [
{
"username": "superfriends",
"links": {
"avatar": {
"href": "http:\/\/i.imgur.com\/ZygP55A.jpg"
}
},
"type": "team"
}
]
}
`

View file

@ -0,0 +1,164 @@
package fixtures
const HookPush = `
{
"actor": {
"username": "emmap1",
"links": {
"avatar": {
"href": "https:\/\/bitbucket-api-assetroot.s3.amazonaws.com\/c\/photos\/2015\/Feb\/26\/3613917261-0-emmap1-avatar_avatar.png"
}
}
},
"repository": {
"links": {
"html": {
"href": "https:\/\/api.bitbucket.org\/team_name\/repo_name"
},
"avatar": {
"href": "https:\/\/api-staging-assetroot.s3.amazonaws.com\/c\/photos\/2014\/Aug\/01\/bitbucket-logo-2629490769-3_avatar.png"
}
},
"full_name": "user_name\/repo_name",
"scm": "git",
"is_private": true
},
"push": {
"changes": [
{
"new": {
"type": "branch",
"name": "name-of-branch",
"target": {
"type": "commit",
"hash": "709d658dc5b6d6afcd46049c2f332ee3f515a67d",
"author": {
"username": "emmap1",
"links": {
"avatar": {
"href": "https:\/\/bitbucket-api-assetroot.s3.amazonaws.com\/c\/photos\/2015\/Feb\/26\/3613917261-0-emmap1-avatar_avatar.png"
}
}
},
"message": "new commit message\n",
"date": "2015-06-09T03:34:49+00:00"
}
}
}
]
}
}
`
const HookPushEmptyHash = `
{
"push": {
"changes": [
{
"new": {
"type": "branch",
"target": { "hash": "" }
}
}
]
}
}
`
const HookPull = `
{
"actor": {
"username": "emmap1",
"links": {
"avatar": {
"href": "https:\/\/bitbucket-api-assetroot.s3.amazonaws.com\/c\/photos\/2015\/Feb\/26\/3613917261-0-emmap1-avatar_avatar.png"
}
}
},
"pullrequest": {
"id": 1,
"title": "Title of pull request",
"description": "Description of pull request",
"state": "OPEN",
"author": {
"username": "emmap1",
"links": {
"avatar": {
"href": "https:\/\/bitbucket-api-assetroot.s3.amazonaws.com\/c\/photos\/2015\/Feb\/26\/3613917261-0-emmap1-avatar_avatar.png"
}
}
},
"source": {
"branch": {
"name": "branch2"
},
"commit": {
"hash": "d3022fc0ca3d"
},
"repository": {
"links": {
"html": {
"href": "https:\/\/api.bitbucket.org\/team_name\/repo_name"
},
"avatar": {
"href": "https:\/\/api-staging-assetroot.s3.amazonaws.com\/c\/photos\/2014\/Aug\/01\/bitbucket-logo-2629490769-3_avatar.png"
}
},
"full_name": "user_name\/repo_name",
"scm": "git",
"is_private": true
}
},
"destination": {
"branch": {
"name": "master"
},
"commit": {
"hash": "ce5965ddd289"
},
"repository": {
"links": {
"html": {
"href": "https:\/\/api.bitbucket.org\/team_name\/repo_name"
},
"avatar": {
"href": "https:\/\/api-staging-assetroot.s3.amazonaws.com\/c\/photos\/2014\/Aug\/01\/bitbucket-logo-2629490769-3_avatar.png"
}
},
"full_name": "user_name\/repo_name",
"scm": "git",
"is_private": true
}
},
"links": {
"self": {
"href": "https:\/\/api.bitbucket.org\/api\/2.0\/pullrequests\/pullrequest_id"
},
"html": {
"href": "https:\/\/api.bitbucket.org\/pullrequest_id"
}
}
},
"repository": {
"links": {
"html": {
"href": "https:\/\/api.bitbucket.org\/team_name\/repo_name"
},
"avatar": {
"href": "https:\/\/api-staging-assetroot.s3.amazonaws.com\/c\/photos\/2014\/Aug\/01\/bitbucket-logo-2629490769-3_avatar.png"
}
},
"full_name": "user_name\/repo_name",
"scm": "git",
"is_private": true
}
}
`
const HookMerged = `
{
"pullrequest": {
"state": "MERGED"
}
}
`

View file

@ -1,102 +0,0 @@
package bitbucket
import (
"testing"
"time"
"github.com/drone/drone/remote/bitbucket/internal"
"github.com/franela/goblin"
"golang.org/x/oauth2"
)
func Test_helper(t *testing.T) {
g := goblin.Goblin(t)
g.Describe("Bitbucket", func() {
g.It("should convert repository lite", func() {
from := &internal.Repo{}
from.FullName = "octocat/hello-world"
from.Owner.Links.Avatar.Href = "http://..."
to := convertRepoLite(from)
g.Assert(to.Avatar).Equal(from.Owner.Links.Avatar.Href)
g.Assert(to.FullName).Equal(from.FullName)
g.Assert(to.Owner).Equal("octocat")
g.Assert(to.Name).Equal("hello-world")
})
g.It("should convert repository", func() {
from := &internal.Repo{
FullName: "octocat/hello-world",
IsPrivate: true,
Scm: "hg",
}
from.Owner.Links.Avatar.Href = "http://..."
from.Links.Html.Href = "https://bitbucket.org/foo/bar"
to := convertRepo(from)
g.Assert(to.Avatar).Equal(from.Owner.Links.Avatar.Href)
g.Assert(to.FullName).Equal(from.FullName)
g.Assert(to.Owner).Equal("octocat")
g.Assert(to.Name).Equal("hello-world")
g.Assert(to.Branch).Equal("default")
g.Assert(to.Kind).Equal(from.Scm)
g.Assert(to.IsPrivate).Equal(from.IsPrivate)
g.Assert(to.Clone).Equal(from.Links.Html.Href)
g.Assert(to.Link).Equal(from.Links.Html.Href)
})
g.It("should convert team", func() {
from := &internal.Account{Login: "octocat"}
from.Links.Avatar.Href = "http://..."
to := convertTeam(from)
g.Assert(to.Avatar).Equal(from.Links.Avatar.Href)
g.Assert(to.Login).Equal(from.Login)
})
g.It("should convert team list", func() {
from := &internal.Account{Login: "octocat"}
from.Links.Avatar.Href = "http://..."
to := convertTeamList([]*internal.Account{from})
g.Assert(to[0].Avatar).Equal(from.Links.Avatar.Href)
g.Assert(to[0].Login).Equal(from.Login)
})
g.It("should convert user", func() {
token := &oauth2.Token{
AccessToken: "foo",
RefreshToken: "bar",
Expiry: time.Now(),
}
user := &internal.Account{Login: "octocat"}
user.Links.Avatar.Href = "http://..."
result := convertUser(user, token)
g.Assert(result.Avatar).Equal(user.Links.Avatar.Href)
g.Assert(result.Login).Equal(user.Login)
g.Assert(result.Token).Equal(token.AccessToken)
g.Assert(result.Token).Equal(token.AccessToken)
g.Assert(result.Secret).Equal(token.RefreshToken)
g.Assert(result.Expiry).Equal(token.Expiry.UTC().Unix())
})
g.It("should use clone url", func() {
repo := &internal.Repo{}
repo.Links.Clone = append(repo.Links.Clone, internal.Link{
Name: "https",
Href: "https://bitbucket.org/foo/bar.git",
})
link := cloneLink(repo)
g.Assert(link).Equal(repo.Links.Clone[0].Href)
})
g.It("should build clone url", func() {
repo := &internal.Repo{}
repo.Links.Html.Href = "https://foo:bar@bitbucket.org/foo/bar.git"
link := cloneLink(repo)
g.Assert(link).Equal("https://bitbucket.org/foo/bar.git")
})
})
}

View file

@ -20,8 +20,6 @@ const (
)
const (
base = "https://api.bitbucket.org"
pathUser = "%s/2.0/user/"
pathEmails = "%s/2.0/user/emails"
pathTeams = "%s/2.0/teams/?%s"
@ -35,52 +33,53 @@ const (
type Client struct {
*http.Client
base string
}
func NewClient(client *http.Client) *Client {
return &Client{client}
func NewClient(url string, client *http.Client) *Client {
return &Client{client, url}
}
func NewClientToken(client, secret string, token *oauth2.Token) *Client {
func NewClientToken(url, client, secret string, token *oauth2.Token) *Client {
config := &oauth2.Config{
ClientID: client,
ClientSecret: secret,
Endpoint: bitbucket.Endpoint,
}
return NewClient(config.Client(oauth2.NoContext, token))
return NewClient(url, config.Client(oauth2.NoContext, token))
}
func (c *Client) FindCurrent() (*Account, error) {
out := new(Account)
uri := fmt.Sprintf(pathUser, base)
uri := fmt.Sprintf(pathUser, c.base)
err := c.do(uri, get, nil, out)
return out, err
}
func (c *Client) ListEmail() (*EmailResp, error) {
out := new(EmailResp)
uri := fmt.Sprintf(pathEmails, base)
uri := fmt.Sprintf(pathEmails, c.base)
err := c.do(uri, get, nil, out)
return out, err
}
func (c *Client) ListTeams(opts *ListTeamOpts) (*AccountResp, error) {
out := new(AccountResp)
uri := fmt.Sprintf(pathTeams, base, opts.Encode())
uri := fmt.Sprintf(pathTeams, c.base, opts.Encode())
err := c.do(uri, get, nil, out)
return out, err
}
func (c *Client) FindRepo(owner, name string) (*Repo, error) {
out := new(Repo)
uri := fmt.Sprintf(pathRepo, base, owner, name)
uri := fmt.Sprintf(pathRepo, c.base, owner, name)
err := c.do(uri, get, nil, out)
return out, err
}
func (c *Client) ListRepos(account string, opts *ListOpts) (*RepoResp, error) {
out := new(RepoResp)
uri := fmt.Sprintf(pathRepos, base, account, opts.Encode())
uri := fmt.Sprintf(pathRepos, c.base, account, opts.Encode())
err := c.do(uri, get, nil, out)
return out, err
}
@ -105,37 +104,37 @@ func (c *Client) ListReposAll(account string) ([]*Repo, error) {
func (c *Client) FindHook(owner, name, id string) (*Hook, error) {
out := new(Hook)
uri := fmt.Sprintf(pathHook, base, owner, name, id)
uri := fmt.Sprintf(pathHook, c.base, owner, name, id)
err := c.do(uri, get, nil, out)
return out, err
}
func (c *Client) ListHooks(owner, name string, opts *ListOpts) (*HookResp, error) {
out := new(HookResp)
uri := fmt.Sprintf(pathHooks, base, owner, name, opts.Encode())
uri := fmt.Sprintf(pathHooks, c.base, owner, name, opts.Encode())
err := c.do(uri, get, nil, out)
return out, err
}
func (c *Client) CreateHook(owner, name string, hook *Hook) error {
uri := fmt.Sprintf(pathHooks, base, owner, name, "")
uri := fmt.Sprintf(pathHooks, c.base, owner, name, "")
return c.do(uri, post, hook, nil)
}
func (c *Client) DeleteHook(owner, name, id string) error {
uri := fmt.Sprintf(pathHook, base, owner, name, id)
uri := fmt.Sprintf(pathHook, c.base, owner, name, id)
return c.do(uri, del, nil, nil)
}
func (c *Client) FindSource(owner, name, revision, path string) (*Source, error) {
out := new(Source)
uri := fmt.Sprintf(pathSource, base, owner, name, revision, path)
uri := fmt.Sprintf(pathSource, c.base, owner, name, revision, path)
err := c.do(uri, get, nil, out)
return out, err
}
func (c *Client) CreateStatus(owner, name, revision string, status *BuildStatus) error {
uri := fmt.Sprintf(pathStatus, base, owner, name, revision)
uri := fmt.Sprintf(pathStatus, c.base, owner, name, revision)
return c.do(uri, post, status, nil)
}

View file

@ -100,27 +100,29 @@ type Source struct {
Size int64 `json:"size"`
}
type Change struct {
New struct {
Type string `json:"type"`
Name string `json:"name"`
Target struct {
Type string `json:"type"`
Hash string `json:"hash"`
Message string `json:"message"`
Date time.Time `json:"date"`
Links Links `json:"links"`
Author struct {
Raw string `json:"raw"`
User Account `json:"user"`
} `json:"author"`
} `json:"target"`
} `json:"new"`
}
type PushHook struct {
Actor Account `json:"actor"`
Repo Repo `json:"repository"`
Push struct {
Changes []struct {
New struct {
Type string `json:"type"`
Name string `json:"name"`
Target struct {
Type string `json:"type"`
Hash string `json:"hash"`
Message string `json:"message"`
Date time.Time `json:"date"`
Links Links `json:"links"`
Author struct {
Raw string `json:"raw"`
User Account `json:"user"`
} `json:"author"`
} `json:"target"`
} `json:"new"`
} `json:"changes"`
Changes []Change `json:"changes"`
} `json:"push"`
}

71
remote/bitbucket/parse.go Normal file
View file

@ -0,0 +1,71 @@
package bitbucket
import (
"encoding/json"
"io/ioutil"
"net/http"
"github.com/drone/drone/model"
"github.com/drone/drone/remote/bitbucket/internal"
)
const (
hookEvent = "X-Event-Key"
hookPush = "repo:push"
hookPullCreated = "pullrequest:created"
hookPullUpdated = "pullrequest:updated"
changeBranch = "branch"
changeNamedBranch = "named_branch"
stateMerged = "MERGED"
stateDeclined = "DECLINED"
stateOpen = "OPEN"
)
// parseHook parses a Bitbucket hook from an http.Request request and returns
// Repo and Build detail. If a hook type is unsupported nil values are returned.
func parseHook(r *http.Request) (*model.Repo, *model.Build, error) {
payload, _ := ioutil.ReadAll(r.Body)
switch r.Header.Get(hookEvent) {
case hookPush:
return parsePushHook(payload)
case hookPullCreated, hookPullUpdated:
return parsePullHook(payload)
}
return nil, nil, nil
}
// parsePushHook parses a push hook and returns the Repo and Build details.
// If the commit type is unsupported nil values are returned.
func parsePushHook(payload []byte) (*model.Repo, *model.Build, error) {
hook := internal.PushHook{}
err := json.Unmarshal(payload, &hook)
if err != nil {
return nil, nil, err
}
for _, change := range hook.Push.Changes {
if change.New.Target.Hash == "" {
continue
}
return convertRepo(&hook.Repo), convertPushHook(&hook, &change), nil
}
return nil, nil, nil
}
// parsePullHook parses a pull request hook and returns the Repo and Build
// details. If the pull request is closed nil values are returned.
func parsePullHook(payload []byte) (*model.Repo, *model.Build, error) {
hook := internal.PullRequestHook{}
if err := json.Unmarshal(payload, &hook); err != nil {
return nil, nil, err
}
if hook.PullRequest.State != stateOpen {
return nil, nil, nil
}
return convertRepo(&hook.Repo), convertPullHook(&hook), nil
}

View file

@ -0,0 +1,104 @@
package bitbucket
import (
"bytes"
"net/http"
"testing"
"github.com/drone/drone/remote/bitbucket/fixtures"
"github.com/franela/goblin"
)
func Test_parser(t *testing.T) {
g := goblin.Goblin(t)
g.Describe("Bitbucket hook parser", func() {
g.It("Should ignore unsupported hook", func() {
buf := bytes.NewBufferString(fixtures.HookPush)
req, _ := http.NewRequest("POST", "/hook", buf)
req.Header = http.Header{}
req.Header.Set(hookEvent, "issue:created")
r, b, err := parseHook(req)
g.Assert(r == nil).IsTrue()
g.Assert(b == nil).IsTrue()
g.Assert(err == nil).IsTrue()
})
g.Describe("Given a pull request hook", func() {
g.It("Should return err when malformed", func() {
buf := bytes.NewBufferString("[]")
req, _ := http.NewRequest("POST", "/hook", buf)
req.Header = http.Header{}
req.Header.Set(hookEvent, hookPullCreated)
_, _, err := parseHook(req)
g.Assert(err != nil).IsTrue()
})
g.It("Should return nil if not open", func() {
buf := bytes.NewBufferString(fixtures.HookMerged)
req, _ := http.NewRequest("POST", "/hook", buf)
req.Header = http.Header{}
req.Header.Set(hookEvent, hookPullCreated)
r, b, err := parseHook(req)
g.Assert(r == nil).IsTrue()
g.Assert(b == nil).IsTrue()
g.Assert(err == nil).IsTrue()
})
g.It("Should return pull request details", func() {
buf := bytes.NewBufferString(fixtures.HookPull)
req, _ := http.NewRequest("POST", "/hook", buf)
req.Header = http.Header{}
req.Header.Set(hookEvent, hookPullCreated)
r, b, err := parseHook(req)
g.Assert(err == nil).IsTrue()
g.Assert(r.FullName).Equal("user_name/repo_name")
g.Assert(b.Commit).Equal("ce5965ddd289")
})
})
g.Describe("Given a push hook", func() {
g.It("Should return err when malformed", func() {
buf := bytes.NewBufferString("[]")
req, _ := http.NewRequest("POST", "/hook", buf)
req.Header = http.Header{}
req.Header.Set(hookEvent, hookPush)
_, _, err := parseHook(req)
g.Assert(err != nil).IsTrue()
})
g.It("Should return nil if missing commit sha", func() {
buf := bytes.NewBufferString(fixtures.HookPushEmptyHash)
req, _ := http.NewRequest("POST", "/hook", buf)
req.Header = http.Header{}
req.Header.Set(hookEvent, hookPush)
r, b, err := parseHook(req)
g.Assert(r == nil).IsTrue()
g.Assert(b == nil).IsTrue()
g.Assert(err == nil).IsTrue()
})
g.It("Should return push details", func() {
buf := bytes.NewBufferString(fixtures.HookPush)
req, _ := http.NewRequest("POST", "/hook", buf)
req.Header = http.Header{}
req.Header.Set(hookEvent, hookPush)
r, b, err := parseHook(req)
g.Assert(err == nil).IsTrue()
g.Assert(r.FullName).Equal("user_name/repo_name")
g.Assert(b.Commit).Equal("709d658dc5b6d6afcd46049c2f332ee3f515a67d")
})
})
})
}