diff --git a/remote/bitbucket/bitbucket.go b/remote/bitbucket/bitbucket.go index 56a4f679e..7f8470212 100644 --- a/remote/bitbucket/bitbucket.go +++ b/remote/bitbucket/bitbucket.go @@ -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) } diff --git a/remote/bitbucket/bitbucket_test.go b/remote/bitbucket/bitbucket_test.go new file mode 100644 index 000000000..6d621788e --- /dev/null +++ b/remote/bitbucket/bitbucket_test.go @@ -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", + } +) diff --git a/remote/bitbucket/const.go b/remote/bitbucket/const.go deleted file mode 100644 index 3dcbb2cb2..000000000 --- a/remote/bitbucket/const.go +++ /dev/null @@ -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 - } -} diff --git a/remote/bitbucket/const_test.go b/remote/bitbucket/const_test.go deleted file mode 100644 index 104947fd7..000000000 --- a/remote/bitbucket/const_test.go +++ /dev/null @@ -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) - }) - }) -} diff --git a/remote/bitbucket/helper.go b/remote/bitbucket/convert.go similarity index 54% rename from remote/bitbucket/helper.go rename to remote/bitbucket/convert.go index 9a516b40f..b64863cd1 100644 --- a/remote/bitbucket/helper.go +++ b/remote/bitbucket/convert.go @@ -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 +} diff --git a/remote/bitbucket/convert_test.go b/remote/bitbucket/convert_test.go new file mode 100644 index 000000000..9f10e2499 --- /dev/null +++ b/remote/bitbucket/convert_test.go @@ -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") + }) + }) +} diff --git a/remote/bitbucket/fixtures/handler.go b/remote/bitbucket/fixtures/handler.go new file mode 100644 index 000000000..a1ce21911 --- /dev/null +++ b/remote/bitbucket/fixtures/handler.go @@ -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" + } + ] +} +` diff --git a/remote/bitbucket/fixtures/hooks.go b/remote/bitbucket/fixtures/hooks.go new file mode 100644 index 000000000..2b68e24e9 --- /dev/null +++ b/remote/bitbucket/fixtures/hooks.go @@ -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" + } +} +` diff --git a/remote/bitbucket/helper_test.go b/remote/bitbucket/helper_test.go deleted file mode 100644 index c978c1291..000000000 --- a/remote/bitbucket/helper_test.go +++ /dev/null @@ -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") - }) - }) -} diff --git a/remote/bitbucket/internal/client.go b/remote/bitbucket/internal/client.go index 932613136..781b3d07b 100644 --- a/remote/bitbucket/internal/client.go +++ b/remote/bitbucket/internal/client.go @@ -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) } diff --git a/remote/bitbucket/internal/types.go b/remote/bitbucket/internal/types.go index 82175e5b2..b92d22e08 100644 --- a/remote/bitbucket/internal/types.go +++ b/remote/bitbucket/internal/types.go @@ -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"` } diff --git a/remote/bitbucket/parse.go b/remote/bitbucket/parse.go new file mode 100644 index 000000000..7ab385d38 --- /dev/null +++ b/remote/bitbucket/parse.go @@ -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 +} diff --git a/remote/bitbucket/parse_test.go b/remote/bitbucket/parse_test.go new file mode 100644 index 000000000..7ff3657be --- /dev/null +++ b/remote/bitbucket/parse_test.go @@ -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") + }) + }) + }) +}