mirror of
https://github.com/woodpecker-ci/woodpecker.git
synced 2024-12-24 09:20:31 +00:00
Merge pull request #2028 from Coding/master
Add integration for Coding.net
This commit is contained in:
commit
6930274d88
16 changed files with 1974 additions and 0 deletions
|
@ -380,6 +380,58 @@ var flags = []cli.Flag{
|
||||||
Name: "stash-skip-verify",
|
Name: "stash-skip-verify",
|
||||||
Usage: "stash skip ssl verification",
|
Usage: "stash skip ssl verification",
|
||||||
},
|
},
|
||||||
|
cli.BoolFlag{
|
||||||
|
EnvVar: "DRONE_CODING",
|
||||||
|
Name: "coding",
|
||||||
|
Usage: "coding driver is enabled",
|
||||||
|
},
|
||||||
|
cli.StringFlag{
|
||||||
|
EnvVar: "DRONE_CODING_URL",
|
||||||
|
Name: "coding-server",
|
||||||
|
Usage: "coding server address",
|
||||||
|
Value: "https://coding.net",
|
||||||
|
},
|
||||||
|
cli.StringFlag{
|
||||||
|
EnvVar: "DRONE_CODING_CLIENT",
|
||||||
|
Name: "coding-client",
|
||||||
|
Usage: "coding oauth2 client id",
|
||||||
|
},
|
||||||
|
cli.StringFlag{
|
||||||
|
EnvVar: "DRONE_CODING_SECRET",
|
||||||
|
Name: "coding-secret",
|
||||||
|
Usage: "coding oauth2 client secret",
|
||||||
|
},
|
||||||
|
cli.StringSliceFlag{
|
||||||
|
EnvVar: "DRONE_CODING_SCOPE",
|
||||||
|
Name: "coding-scope",
|
||||||
|
Usage: "coding oauth scope",
|
||||||
|
Value: &cli.StringSlice{
|
||||||
|
"user",
|
||||||
|
"project",
|
||||||
|
"project:depot",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
cli.StringFlag{
|
||||||
|
EnvVar: "DRONE_CODING_GIT_MACHINE",
|
||||||
|
Name: "coding-git-machine",
|
||||||
|
Usage: "coding machine name",
|
||||||
|
Value: "git.coding.net",
|
||||||
|
},
|
||||||
|
cli.StringFlag{
|
||||||
|
EnvVar: "DRONE_CODING_GIT_USERNAME",
|
||||||
|
Name: "coding-git-username",
|
||||||
|
Usage: "coding machine user username",
|
||||||
|
},
|
||||||
|
cli.StringFlag{
|
||||||
|
EnvVar: "DRONE_CODING_GIT_PASSWORD",
|
||||||
|
Name: "coding-git-password",
|
||||||
|
Usage: "coding machine user password",
|
||||||
|
},
|
||||||
|
cli.BoolFlag{
|
||||||
|
EnvVar: "DRONE_CODING_SKIP_VERIFY",
|
||||||
|
Name: "coding-skip-verify",
|
||||||
|
Usage: "coding skip ssl verification",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
func server(c *cli.Context) error {
|
func server(c *cli.Context) error {
|
||||||
|
|
|
@ -10,6 +10,7 @@ import (
|
||||||
"github.com/drone/drone/remote"
|
"github.com/drone/drone/remote"
|
||||||
"github.com/drone/drone/remote/bitbucket"
|
"github.com/drone/drone/remote/bitbucket"
|
||||||
"github.com/drone/drone/remote/bitbucketserver"
|
"github.com/drone/drone/remote/bitbucketserver"
|
||||||
|
"github.com/drone/drone/remote/coding"
|
||||||
"github.com/drone/drone/remote/gitea"
|
"github.com/drone/drone/remote/gitea"
|
||||||
"github.com/drone/drone/remote/github"
|
"github.com/drone/drone/remote/github"
|
||||||
"github.com/drone/drone/remote/gitlab"
|
"github.com/drone/drone/remote/gitlab"
|
||||||
|
@ -62,6 +63,8 @@ func SetupRemote(c *cli.Context) (remote.Remote, error) {
|
||||||
return setupGogs(c)
|
return setupGogs(c)
|
||||||
case c.Bool("gitea"):
|
case c.Bool("gitea"):
|
||||||
return setupGitea(c)
|
return setupGitea(c)
|
||||||
|
case c.Bool("coding"):
|
||||||
|
return setupCoding(c)
|
||||||
default:
|
default:
|
||||||
return nil, fmt.Errorf("version control system not configured")
|
return nil, fmt.Errorf("version control system not configured")
|
||||||
}
|
}
|
||||||
|
@ -139,4 +142,18 @@ func setupGithub(c *cli.Context) (remote.Remote, error) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// helper function to setup the Coding remote from the CLI arguments.
|
||||||
|
func setupCoding(c *cli.Context) (remote.Remote, error) {
|
||||||
|
return coding.New(coding.Opts{
|
||||||
|
URL: c.String("coding-server"),
|
||||||
|
Client: c.String("coding-client"),
|
||||||
|
Secret: c.String("coding-secret"),
|
||||||
|
Scopes: c.StringSlice("coding-scope"),
|
||||||
|
Machine: c.String("coding-git-machine"),
|
||||||
|
Username: c.String("coding-git-username"),
|
||||||
|
Password: c.String("coding-git-password"),
|
||||||
|
SkipVerify: c.Bool("coding-skip-verify"),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func before(c *cli.Context) error { return nil }
|
func before(c *cli.Context) error { return nil }
|
||||||
|
|
334
remote/coding/coding.go
Normal file
334
remote/coding/coding.go
Normal file
|
@ -0,0 +1,334 @@
|
||||||
|
package coding
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/tls"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/drone/drone/model"
|
||||||
|
"github.com/drone/drone/remote"
|
||||||
|
"github.com/drone/drone/remote/coding/internal"
|
||||||
|
"github.com/drone/drone/shared/httputil"
|
||||||
|
|
||||||
|
"golang.org/x/net/context"
|
||||||
|
"golang.org/x/oauth2"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
defaultURL = "https://coding.net" // Default Coding URL
|
||||||
|
)
|
||||||
|
|
||||||
|
// Opts defines configuration options.
|
||||||
|
type Opts struct {
|
||||||
|
URL string // Coding server url.
|
||||||
|
Client string // Coding oauth client id.
|
||||||
|
Secret string // Coding oauth client secret.
|
||||||
|
Scopes []string // Coding oauth scopes.
|
||||||
|
Machine string // Optional machine name.
|
||||||
|
Username string // Optional machine account username.
|
||||||
|
Password string // Optional machine account password.
|
||||||
|
SkipVerify bool // Skip ssl verification.
|
||||||
|
}
|
||||||
|
|
||||||
|
// New returns a Remote implementation that integrates with a Coding Platform or
|
||||||
|
// Coding Enterprise version control hosting provider.
|
||||||
|
func New(opts Opts) (remote.Remote, error) {
|
||||||
|
remote := &Coding{
|
||||||
|
URL: defaultURL,
|
||||||
|
Client: opts.Client,
|
||||||
|
Secret: opts.Secret,
|
||||||
|
Scopes: opts.Scopes,
|
||||||
|
Machine: opts.Machine,
|
||||||
|
Username: opts.Username,
|
||||||
|
Password: opts.Password,
|
||||||
|
SkipVerify: opts.SkipVerify,
|
||||||
|
}
|
||||||
|
if opts.URL != defaultURL {
|
||||||
|
remote.URL = strings.TrimSuffix(opts.URL, "/")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hack to enable oauth2 access in coding's implementation
|
||||||
|
oauth2.RegisterBrokenAuthHeaderProvider(remote.URL)
|
||||||
|
return remote, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type Coding struct {
|
||||||
|
URL string
|
||||||
|
Client string
|
||||||
|
Secret string
|
||||||
|
Scopes []string
|
||||||
|
Machine string
|
||||||
|
Username string
|
||||||
|
Password string
|
||||||
|
SkipVerify bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// Login authenticates the session and returns the
|
||||||
|
// remote user details.
|
||||||
|
func (c *Coding) Login(res http.ResponseWriter, req *http.Request) (*model.User, error) {
|
||||||
|
config := c.newConfig(httputil.GetURL(req))
|
||||||
|
|
||||||
|
// get the OAuth errors
|
||||||
|
if err := req.FormValue("error"); err != "" {
|
||||||
|
return nil, &remote.AuthError{
|
||||||
|
Err: err,
|
||||||
|
Description: req.FormValue("error_description"),
|
||||||
|
URI: req.FormValue("error_uri"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// get the OAuth code
|
||||||
|
code := req.FormValue("code")
|
||||||
|
if len(code) == 0 {
|
||||||
|
http.Redirect(res, req, config.AuthCodeURL("drone"), http.StatusSeeOther)
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
token, err := config.Exchange(c.newContext(), code)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
user, err := c.newClientToken(token.AccessToken, token.RefreshToken).GetCurrentUser()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &model.User{
|
||||||
|
Login: user.GlobalKey,
|
||||||
|
Email: user.Email,
|
||||||
|
Token: token.AccessToken,
|
||||||
|
Secret: token.RefreshToken,
|
||||||
|
Expiry: token.Expiry.UTC().Unix(),
|
||||||
|
Avatar: c.resourceLink(user.Avatar),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auth authenticates the session and returns the remote user
|
||||||
|
// login for the given token and secret
|
||||||
|
func (c *Coding) Auth(token, secret string) (string, error) {
|
||||||
|
user, err := c.newClientToken(token, secret).GetCurrentUser()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return user.GlobalKey, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Refresh refreshes an oauth token and expiration for the given
|
||||||
|
// user. It returns true if the token was refreshed, false if the
|
||||||
|
// token was not refreshed, and error if it failed to refersh.
|
||||||
|
func (c *Coding) Refresh(u *model.User) (bool, error) {
|
||||||
|
config := c.newConfig("")
|
||||||
|
source := config.TokenSource(c.newContext(), &oauth2.Token{RefreshToken: u.Secret})
|
||||||
|
token, err := source.Token()
|
||||||
|
if err != nil || len(token.AccessToken) == 0 {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
u.Token = token.AccessToken
|
||||||
|
u.Secret = token.RefreshToken
|
||||||
|
u.Expiry = token.Expiry.UTC().Unix()
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Teams fetches a list of team memberships from the remote system.
|
||||||
|
func (c *Coding) Teams(u *model.User) ([]*model.Team, error) {
|
||||||
|
// EMPTY: not implemented in Coding OAuth API
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// TeamPerm fetches the named organization permissions from
|
||||||
|
// the remote system for the specified user.
|
||||||
|
func (c *Coding) TeamPerm(u *model.User, org string) (*model.Perm, error) {
|
||||||
|
// EMPTY: not implemented in Coding OAuth API
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Repo fetches the named repository from the remote system.
|
||||||
|
func (c *Coding) Repo(u *model.User, owner, repo string) (*model.Repo, error) {
|
||||||
|
project, err := c.newClient(u).GetProject(owner, repo)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
depot, err := c.newClient(u).GetDepot(owner, repo)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &model.Repo{
|
||||||
|
Owner: project.Owner,
|
||||||
|
Name: project.Name,
|
||||||
|
FullName: projectFullName(project.Owner, project.Name),
|
||||||
|
Avatar: c.resourceLink(project.Icon),
|
||||||
|
Link: c.resourceLink(project.DepotPath),
|
||||||
|
Kind: model.RepoGit,
|
||||||
|
Clone: project.HttpsURL,
|
||||||
|
Branch: depot.DefaultBranch,
|
||||||
|
IsPrivate: !project.IsPublic,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Repos fetches a list of repos from the remote system.
|
||||||
|
func (c *Coding) Repos(u *model.User) ([]*model.Repo, error) {
|
||||||
|
projectList, err := c.newClient(u).GetProjectList()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
repos := make([]*model.Repo, 0)
|
||||||
|
for _, project := range projectList {
|
||||||
|
depot, err := c.newClient(u).GetDepot(project.Owner, project.Name)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
repo := &model.Repo{
|
||||||
|
Owner: project.Owner,
|
||||||
|
Name: project.Name,
|
||||||
|
FullName: projectFullName(project.Owner, project.Name),
|
||||||
|
Avatar: c.resourceLink(project.Icon),
|
||||||
|
Link: c.resourceLink(project.DepotPath),
|
||||||
|
Kind: model.RepoGit,
|
||||||
|
Clone: project.HttpsURL,
|
||||||
|
Branch: depot.DefaultBranch,
|
||||||
|
IsPrivate: !project.IsPublic,
|
||||||
|
}
|
||||||
|
repos = append(repos, repo)
|
||||||
|
}
|
||||||
|
return repos, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Perm fetches the named repository permissions from
|
||||||
|
// the remote system for the specified user.
|
||||||
|
func (c *Coding) Perm(u *model.User, owner, repo string) (*model.Perm, error) {
|
||||||
|
project, err := c.newClient(u).GetProject(owner, repo)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if project.Role == "owner" || project.Role == "admin" {
|
||||||
|
return &model.Perm{Pull: true, Push: true, Admin: true}, nil
|
||||||
|
}
|
||||||
|
if project.Role == "member" {
|
||||||
|
return &model.Perm{Pull: true, Push: true, Admin: false}, nil
|
||||||
|
}
|
||||||
|
return &model.Perm{Pull: false, Push: false, Admin: false}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// File fetches a file from the remote repository and returns in string
|
||||||
|
// format.
|
||||||
|
func (c *Coding) File(u *model.User, r *model.Repo, b *model.Build, f string) ([]byte, error) {
|
||||||
|
data, err := c.newClient(u).GetFile(r.Owner, r.Name, b.Commit, f)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return data, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// FileRef fetches a file from the remote repository for the given ref
|
||||||
|
// and returns in string format.
|
||||||
|
func (c *Coding) FileRef(u *model.User, r *model.Repo, ref, f string) ([]byte, error) {
|
||||||
|
data, err := c.newClient(u).GetFile(r.Owner, r.Name, ref, f)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return data, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Status sends the commit status to the remote system.
|
||||||
|
func (c *Coding) Status(u *model.User, r *model.Repo, b *model.Build, link string) error {
|
||||||
|
// EMPTY: not implemented in Coding OAuth API
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Netrc returns a .netrc file that can be used to clone
|
||||||
|
// private repositories from a remote system.
|
||||||
|
func (c *Coding) Netrc(u *model.User, r *model.Repo) (*model.Netrc, error) {
|
||||||
|
if c.Password != "" {
|
||||||
|
return &model.Netrc{
|
||||||
|
Login: c.Username,
|
||||||
|
Password: c.Password,
|
||||||
|
Machine: c.Machine,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
return &model.Netrc{
|
||||||
|
Login: u.Token,
|
||||||
|
Password: "x-oauth-basic",
|
||||||
|
Machine: c.Machine,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Activate activates a repository by creating the post-commit hook.
|
||||||
|
func (c *Coding) Activate(u *model.User, r *model.Repo, link string) error {
|
||||||
|
return c.newClient(u).AddWebhook(r.Owner, r.Name, link)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deactivate deactivates a repository by removing all previously created
|
||||||
|
// post-commit hooks matching the given link.
|
||||||
|
func (c *Coding) Deactivate(u *model.User, r *model.Repo, link string) error {
|
||||||
|
return c.newClient(u).RemoveWebhook(r.Owner, r.Name, link)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hook parses the post-commit hook from the Request body and returns the
|
||||||
|
// required data in a standard format.
|
||||||
|
func (c *Coding) Hook(r *http.Request) (*model.Repo, *model.Build, error) {
|
||||||
|
repo, build, err := parseHook(r)
|
||||||
|
if build != nil {
|
||||||
|
build.Avatar = c.resourceLink(build.Avatar)
|
||||||
|
}
|
||||||
|
return repo, build, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// helper function to return the Coding oauth2 context using an HTTPClient that
|
||||||
|
// disables TLS verification if disabled in the remote settings.
|
||||||
|
func (c *Coding) newContext() context.Context {
|
||||||
|
if !c.SkipVerify {
|
||||||
|
return oauth2.NoContext
|
||||||
|
}
|
||||||
|
return context.WithValue(nil, oauth2.HTTPClient, &http.Client{
|
||||||
|
Transport: &http.Transport{
|
||||||
|
Proxy: http.ProxyFromEnvironment,
|
||||||
|
TLSClientConfig: &tls.Config{
|
||||||
|
InsecureSkipVerify: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// helper function to return the Coding oauth2 config
|
||||||
|
func (c *Coding) newConfig(redirect string) *oauth2.Config {
|
||||||
|
return &oauth2.Config{
|
||||||
|
ClientID: c.Client,
|
||||||
|
ClientSecret: c.Secret,
|
||||||
|
Scopes: []string{strings.Join(c.Scopes, ",")},
|
||||||
|
Endpoint: oauth2.Endpoint{
|
||||||
|
AuthURL: fmt.Sprintf("%s/oauth_authorize.html", c.URL),
|
||||||
|
TokenURL: fmt.Sprintf("%s/api/oauth/access_token_v2", c.URL),
|
||||||
|
},
|
||||||
|
RedirectURL: fmt.Sprintf("%s/authorize", redirect),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// helper function to return the Coding oauth2 client
|
||||||
|
func (c *Coding) newClient(u *model.User) *internal.Client {
|
||||||
|
return c.newClientToken(u.Token, u.Secret)
|
||||||
|
}
|
||||||
|
|
||||||
|
// helper function to return the Coding oauth2 client
|
||||||
|
func (c *Coding) newClientToken(token, secret string) *internal.Client {
|
||||||
|
client := &http.Client{
|
||||||
|
Transport: &http.Transport{
|
||||||
|
Proxy: http.ProxyFromEnvironment,
|
||||||
|
TLSClientConfig: &tls.Config{
|
||||||
|
InsecureSkipVerify: c.SkipVerify,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return internal.NewClient(c.URL, "/api", token, "drone", client)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Coding) resourceLink(resourcePath string) string {
|
||||||
|
if strings.HasPrefix(resourcePath, "http") {
|
||||||
|
return resourcePath
|
||||||
|
}
|
||||||
|
return c.URL + resourcePath
|
||||||
|
}
|
295
remote/coding/coding_test.go
Normal file
295
remote/coding/coding_test.go
Normal file
|
@ -0,0 +1,295 @@
|
||||||
|
package coding
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/drone/drone/model"
|
||||||
|
"github.com/drone/drone/remote/coding/fixtures"
|
||||||
|
|
||||||
|
"github.com/franela/goblin"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Test_coding(t *testing.T) {
|
||||||
|
gin.SetMode(gin.TestMode)
|
||||||
|
|
||||||
|
s := httptest.NewServer(fixtures.Handler())
|
||||||
|
c := &Coding{URL: s.URL}
|
||||||
|
|
||||||
|
g := goblin.Goblin(t)
|
||||||
|
g.Describe("Coding", func() {
|
||||||
|
|
||||||
|
g.After(func() {
|
||||||
|
s.Close()
|
||||||
|
})
|
||||||
|
|
||||||
|
g.Describe("Creating a remote", func() {
|
||||||
|
g.It("Should return client with specified options", func() {
|
||||||
|
remote, _ := New(Opts{
|
||||||
|
URL: "https://coding.net",
|
||||||
|
Client: "KTNF2ALdm3ofbtxLh6IbV95Ro5AKWJUP",
|
||||||
|
Secret: "zVtxJrKhNhBcNyqCz1NggNAAmehAxnRO3Z0fXmCp",
|
||||||
|
Scopes: []string{"user", "project", "project:depot"},
|
||||||
|
Machine: "git.coding.net",
|
||||||
|
Username: "someuser",
|
||||||
|
Password: "password",
|
||||||
|
SkipVerify: true,
|
||||||
|
})
|
||||||
|
g.Assert(remote.(*Coding).URL).Equal("https://coding.net")
|
||||||
|
g.Assert(remote.(*Coding).Client).Equal("KTNF2ALdm3ofbtxLh6IbV95Ro5AKWJUP")
|
||||||
|
g.Assert(remote.(*Coding).Secret).Equal("zVtxJrKhNhBcNyqCz1NggNAAmehAxnRO3Z0fXmCp")
|
||||||
|
g.Assert(remote.(*Coding).Scopes).Equal([]string{"user", "project", "project:depot"})
|
||||||
|
g.Assert(remote.(*Coding).Machine).Equal("git.coding.net")
|
||||||
|
g.Assert(remote.(*Coding).Username).Equal("someuser")
|
||||||
|
g.Assert(remote.(*Coding).Password).Equal("password")
|
||||||
|
g.Assert(remote.(*Coding).SkipVerify).Equal(true)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
g.Describe("Given an authorization request", func() {
|
||||||
|
g.It("Should redirect to authorize", func() {
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
r, _ := http.NewRequest("GET", "", nil)
|
||||||
|
_, err := c.Login(w, r)
|
||||||
|
g.Assert(err == nil).IsTrue()
|
||||||
|
g.Assert(w.Code).Equal(http.StatusSeeOther)
|
||||||
|
})
|
||||||
|
g.It("Should return authenticated user", func() {
|
||||||
|
r, _ := http.NewRequest("GET", "?code=code", nil)
|
||||||
|
u, err := c.Login(nil, r)
|
||||||
|
g.Assert(err == nil).IsTrue()
|
||||||
|
g.Assert(u.Login).Equal(fakeUser.Login)
|
||||||
|
g.Assert(u.Token).Equal(fakeUser.Token)
|
||||||
|
g.Assert(u.Secret).Equal(fakeUser.Secret)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
g.Describe("Given an access token", func() {
|
||||||
|
g.It("Should return the anthenticated user", func() {
|
||||||
|
login, err := c.Auth(
|
||||||
|
fakeUser.Token,
|
||||||
|
fakeUser.Secret,
|
||||||
|
)
|
||||||
|
g.Assert(err == nil).IsTrue()
|
||||||
|
g.Assert(login).Equal(fakeUser.Login)
|
||||||
|
})
|
||||||
|
g.It("Should handle a failure to resolve user", func() {
|
||||||
|
_, err := c.Auth(
|
||||||
|
fakeUserNotFound.Token,
|
||||||
|
fakeUserNotFound.Secret,
|
||||||
|
)
|
||||||
|
g.Assert(err != nil).IsTrue()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
g.Describe("Given a refresh token", func() {
|
||||||
|
g.It("Should return a refresh access token", func() {
|
||||||
|
ok, err := c.Refresh(fakeUserRefresh)
|
||||||
|
g.Assert(err == nil).IsTrue()
|
||||||
|
g.Assert(ok).IsTrue()
|
||||||
|
g.Assert(fakeUserRefresh.Token).Equal("VDZupx0usVRV4oOd1FCu4xUxgk8SY0TK")
|
||||||
|
g.Assert(fakeUserRefresh.Secret).Equal("BenBQq7TWZ7Cp0aUM47nQjTz2QHNmTWcPctB609n")
|
||||||
|
})
|
||||||
|
g.It("Should handle an invalid refresh token", func() {
|
||||||
|
ok, _ := c.Refresh(fakeUserRefreshInvalid)
|
||||||
|
g.Assert(ok).IsFalse()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
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.Assert(repo.Avatar).Equal(s.URL + fakeRepo.Avatar)
|
||||||
|
g.Assert(repo.Link).Equal(s.URL + fakeRepo.Link)
|
||||||
|
g.Assert(repo.Kind).Equal(fakeRepo.Kind)
|
||||||
|
g.Assert(repo.Clone).Equal(fakeRepo.Clone)
|
||||||
|
g.Assert(repo.Branch).Equal(fakeRepo.Branch)
|
||||||
|
g.Assert(repo.IsPrivate).Equal(fakeRepo.IsPrivate)
|
||||||
|
})
|
||||||
|
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 authorize admin access for project owner", func() {
|
||||||
|
perm, err := c.Perm(fakeUser, "demo1", "perm_owner")
|
||||||
|
g.Assert(err == nil).IsTrue()
|
||||||
|
g.Assert(perm.Pull).IsTrue()
|
||||||
|
g.Assert(perm.Push).IsTrue()
|
||||||
|
g.Assert(perm.Admin).IsTrue()
|
||||||
|
})
|
||||||
|
g.It("Should authorize admin access for project admin", func() {
|
||||||
|
perm, err := c.Perm(fakeUser, "demo1", "perm_admin")
|
||||||
|
g.Assert(err == nil).IsTrue()
|
||||||
|
g.Assert(perm.Pull).IsTrue()
|
||||||
|
g.Assert(perm.Push).IsTrue()
|
||||||
|
g.Assert(perm.Admin).IsTrue()
|
||||||
|
})
|
||||||
|
g.It("Should authorize read access for project member", func() {
|
||||||
|
perm, err := c.Perm(fakeUser, "demo1", "perm_member")
|
||||||
|
g.Assert(err == nil).IsTrue()
|
||||||
|
g.Assert(perm.Pull).IsTrue()
|
||||||
|
g.Assert(perm.Push).IsTrue()
|
||||||
|
g.Assert(perm.Admin).IsFalse()
|
||||||
|
})
|
||||||
|
g.It("Should authorize no access for project guest", func() {
|
||||||
|
perm, err := c.Perm(fakeUser, "demo1", "perm_guest")
|
||||||
|
g.Assert(err == nil).IsTrue()
|
||||||
|
g.Assert(perm.Pull).IsFalse()
|
||||||
|
g.Assert(perm.Push).IsFalse()
|
||||||
|
g.Assert(perm.Admin).IsFalse()
|
||||||
|
})
|
||||||
|
g.It("Should handle not found errors", func() {
|
||||||
|
_, err := c.Perm(
|
||||||
|
fakeUser,
|
||||||
|
fakeRepoNotFound.Owner,
|
||||||
|
fakeRepoNotFound.Name,
|
||||||
|
)
|
||||||
|
g.Assert(err != nil).IsTrue()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
g.Describe("When downloading a file", func() {
|
||||||
|
g.It("Should return file for specified build", func() {
|
||||||
|
data, err := c.File(fakeUser, fakeRepo, fakeBuild, ".drone.yml")
|
||||||
|
g.Assert(err == nil).IsTrue()
|
||||||
|
g.Assert(string(data)).Equal("pipeline:\n test:\n image: golang:1.6\n commands:\n - go test\n")
|
||||||
|
})
|
||||||
|
g.It("Should return file for specified ref", func() {
|
||||||
|
data, err := c.FileRef(fakeUser, fakeRepo, "master", ".drone.yml")
|
||||||
|
g.Assert(err == nil).IsTrue()
|
||||||
|
g.Assert(string(data)).Equal("pipeline:\n test:\n image: golang:1.6\n commands:\n - go test\n")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
g.Describe("When requesting a netrc config", func() {
|
||||||
|
g.It("Should return the netrc file for global credential", func() {
|
||||||
|
remote, _ := New(Opts{
|
||||||
|
Machine: "git.coding.net",
|
||||||
|
Username: "someuser",
|
||||||
|
Password: "password",
|
||||||
|
})
|
||||||
|
netrc, err := remote.Netrc(fakeUser, nil)
|
||||||
|
g.Assert(err == nil).IsTrue()
|
||||||
|
g.Assert(netrc.Login).Equal("someuser")
|
||||||
|
g.Assert(netrc.Password).Equal("password")
|
||||||
|
g.Assert(netrc.Machine).Equal("git.coding.net")
|
||||||
|
})
|
||||||
|
g.It("Should return the netrc file for specified user", func() {
|
||||||
|
remote, _ := New(Opts{
|
||||||
|
Machine: "git.coding.net",
|
||||||
|
})
|
||||||
|
netrc, err := remote.Netrc(fakeUser, nil)
|
||||||
|
g.Assert(err == nil).IsTrue()
|
||||||
|
g.Assert(netrc.Login).Equal(fakeUser.Token)
|
||||||
|
g.Assert(netrc.Password).Equal("x-oauth-basic")
|
||||||
|
g.Assert(netrc.Machine).Equal("git.coding.net")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
g.Describe("When activating a repository", func() {
|
||||||
|
g.It("Should create the hook", func() {
|
||||||
|
err := c.Activate(fakeUser, fakeRepo, "http://127.0.0.1")
|
||||||
|
g.Assert(err == nil).IsTrue()
|
||||||
|
})
|
||||||
|
g.It("Should update the hook when exists", func() {
|
||||||
|
err := c.Activate(fakeUser, fakeRepo, "http://127.0.0.2")
|
||||||
|
g.Assert(err == nil).IsTrue()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
g.Describe("When deactivating a repository", func() {
|
||||||
|
g.It("Should successfully remove hook", func() {
|
||||||
|
err := c.Deactivate(fakeUser, fakeRepo, "http://127.0.0.3")
|
||||||
|
g.Assert(err == nil).IsTrue()
|
||||||
|
})
|
||||||
|
g.It("Should successfully deactivate when hook already removed", func() {
|
||||||
|
err := c.Deactivate(fakeUser, fakeRepo, "http://127.0.0.4")
|
||||||
|
g.Assert(err == nil).IsTrue()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
g.Describe("When parsing post-commit hook body", func() {
|
||||||
|
g.It("Should parse the hook", func() {
|
||||||
|
buf := bytes.NewBufferString(fixtures.PushHook)
|
||||||
|
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("demo1/test1")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
fakeUser = &model.User{
|
||||||
|
Login: "demo1",
|
||||||
|
Token: "KTNF2ALdm3ofbtxLh6IbV95Ro5AKWJUP",
|
||||||
|
Secret: "zVtxJrKhNhBcNyqCz1NggNAAmehAxnRO3Z0fXmCp",
|
||||||
|
}
|
||||||
|
|
||||||
|
fakeUserNotFound = &model.User{
|
||||||
|
Login: "demo1",
|
||||||
|
Token: "8DpqlE0hI6yr5MLlq8ysAL4p72cKGwT0",
|
||||||
|
Secret: "8Em2dkFE8Xsze88Ar8LMG7TF4CO3VCQMgpKa0VCm",
|
||||||
|
}
|
||||||
|
|
||||||
|
fakeUserRefresh = &model.User{
|
||||||
|
Login: "demo1",
|
||||||
|
Secret: "i9i0HQqNR8bTY4rALYEF2itayFJNbnzC1eMFppwT",
|
||||||
|
}
|
||||||
|
|
||||||
|
fakeUserRefreshInvalid = &model.User{
|
||||||
|
Login: "demo1",
|
||||||
|
Secret: "invalid_refresh_token",
|
||||||
|
}
|
||||||
|
|
||||||
|
fakeRepo = &model.Repo{
|
||||||
|
Owner: "demo1",
|
||||||
|
Name: "test1",
|
||||||
|
FullName: "demo1/test1",
|
||||||
|
Avatar: "/static/project_icon/scenery-5.png",
|
||||||
|
Link: "/u/gilala/p/abp/git",
|
||||||
|
Kind: model.RepoGit,
|
||||||
|
Clone: "https://git.coding.net/demo1/test1.git",
|
||||||
|
Branch: "master",
|
||||||
|
IsPrivate: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
fakeRepoNotFound = &model.Repo{
|
||||||
|
Owner: "not_found_owner",
|
||||||
|
Name: "not_found_project",
|
||||||
|
}
|
||||||
|
|
||||||
|
fakeRepos = []*model.RepoLite{
|
||||||
|
&model.RepoLite{
|
||||||
|
Owner: "demo1",
|
||||||
|
Name: "test1",
|
||||||
|
FullName: "demo1/test1",
|
||||||
|
Avatar: "/static/project_icon/scenery-5.png",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
fakeBuild = &model.Build{
|
||||||
|
Commit: "4504a072cc",
|
||||||
|
}
|
||||||
|
)
|
313
remote/coding/fixtures/handler.go
Normal file
313
remote/coding/fixtures/handler.go
Normal file
|
@ -0,0 +1,313 @@
|
||||||
|
package fixtures
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Handler returns an http.Handler that is capable of handing a variety of mock
|
||||||
|
// Coding requests and returns mock responses.
|
||||||
|
func Handler() http.Handler {
|
||||||
|
gin.SetMode(gin.TestMode)
|
||||||
|
|
||||||
|
e := gin.New()
|
||||||
|
e.POST("/api/oauth/access_token_v2", getToken)
|
||||||
|
e.GET("/api/account/current_user", getUser)
|
||||||
|
e.GET("/api/user/:gk/project/:prj", getProject)
|
||||||
|
e.GET("/api/user/:gk/project/:prj/git", getDepot)
|
||||||
|
e.GET("/api/user/:gk/project/:prj/git/blob/:ref/:path", getFile)
|
||||||
|
e.GET("/api/user/:gk/project/:prj/git/hooks", getHooks)
|
||||||
|
e.POST("/api/user/:gk/project/:prj/git/hook", postHook)
|
||||||
|
e.PUT("/api/user/:gk/project/:prj/git/hook/:id", putHook)
|
||||||
|
e.DELETE("/api/user/:gk/project/:prj/git/hook/:id", deleteHook)
|
||||||
|
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
|
||||||
|
func getToken(c *gin.Context) {
|
||||||
|
c.Header("Content-Type", "application/json;charset=UTF-8")
|
||||||
|
switch c.PostForm("grant_type") {
|
||||||
|
case "refresh_token":
|
||||||
|
switch c.PostForm("refresh_token") {
|
||||||
|
case "i9i0HQqNR8bTY4rALYEF2itayFJNbnzC1eMFppwT":
|
||||||
|
c.String(200, refreshedTokenPayload)
|
||||||
|
default:
|
||||||
|
c.String(200, invalidRefreshTokenPayload)
|
||||||
|
}
|
||||||
|
case "authorization_code":
|
||||||
|
fallthrough
|
||||||
|
default:
|
||||||
|
switch c.PostForm("code") {
|
||||||
|
case "code":
|
||||||
|
c.String(200, tokenPayload)
|
||||||
|
default:
|
||||||
|
c.String(200, invalidCodePayload)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getUser(c *gin.Context) {
|
||||||
|
c.Header("Content-Type", "application/json;charset=UTF-8")
|
||||||
|
switch c.Query("access_token") {
|
||||||
|
case "KTNF2ALdm3ofbtxLh6IbV95Ro5AKWJUP":
|
||||||
|
c.String(200, userPayload)
|
||||||
|
default:
|
||||||
|
c.String(200, userNotFoundPayload)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getProject(c *gin.Context) {
|
||||||
|
c.Header("Content-Type", "application/json;charset=UTF-8")
|
||||||
|
switch fmt.Sprintf("%s/%s", c.Param("gk"), c.Param("prj")) {
|
||||||
|
case "demo1/test1":
|
||||||
|
c.String(200, fakeProjectPayload)
|
||||||
|
case "demo1/perm_owner":
|
||||||
|
c.String(200, fakePermOwnerPayload)
|
||||||
|
case "demo1/perm_admin":
|
||||||
|
c.String(200, fakePermAdminPayload)
|
||||||
|
case "demo1/perm_member":
|
||||||
|
c.String(200, fakePermMemberPayload)
|
||||||
|
case "demo1/perm_guest":
|
||||||
|
c.String(200, fakePermGuestPayload)
|
||||||
|
default:
|
||||||
|
c.String(200, projectNotFoundPayload)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getDepot(c *gin.Context) {
|
||||||
|
c.Header("Content-Type", "application/json;charset=UTF-8")
|
||||||
|
switch fmt.Sprintf("%s/%s", c.Param("gk"), c.Param("prj")) {
|
||||||
|
case "demo1/test1":
|
||||||
|
c.String(200, fakeDepotPayload)
|
||||||
|
default:
|
||||||
|
c.String(200, projectNotFoundPayload)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getProjects(c *gin.Context) {
|
||||||
|
c.Header("Content-Type", "application/json;charset=UTF-8")
|
||||||
|
c.String(200, fakeProjectsPayload)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getFile(c *gin.Context) {
|
||||||
|
c.Header("Content-Type", "application/json;charset=UTF-8")
|
||||||
|
switch fmt.Sprintf("%s/%s/%s/%s", c.Param("gk"), c.Param("prj"), c.Param("ref"), c.Param("path")) {
|
||||||
|
case "demo1/test1/master/.drone.yml", "demo1/test1/4504a072cc/.drone.yml":
|
||||||
|
c.String(200, fakeFilePayload)
|
||||||
|
default:
|
||||||
|
c.String(200, fileNotFoundPayload)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getHooks(c *gin.Context) {
|
||||||
|
c.Header("Content-Type", "application/json;charset=UTF-8")
|
||||||
|
c.String(200, fakeHooksPayload)
|
||||||
|
}
|
||||||
|
|
||||||
|
func postHook(c *gin.Context) {
|
||||||
|
c.Header("Content-Type", "application/json;charset=UTF-8")
|
||||||
|
switch c.PostForm("hook_url") {
|
||||||
|
case "http://127.0.0.1":
|
||||||
|
c.String(200, `{"code":0}`)
|
||||||
|
default:
|
||||||
|
c.String(200, `{"code":1}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func putHook(c *gin.Context) {
|
||||||
|
c.Header("Content-Type", "application/json;charset=UTF-8")
|
||||||
|
switch c.Param("id") {
|
||||||
|
case "2":
|
||||||
|
c.String(200, `{"code":0}`)
|
||||||
|
default:
|
||||||
|
c.String(200, `{"code":1}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func deleteHook(c *gin.Context) {
|
||||||
|
c.Header("Content-Type", "application/json;charset=UTF-8")
|
||||||
|
switch c.Param("id") {
|
||||||
|
case "3":
|
||||||
|
c.String(200, `{"code":0}`)
|
||||||
|
default:
|
||||||
|
c.String(200, `{"code":1}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const tokenPayload = `
|
||||||
|
{
|
||||||
|
"access_token":"KTNF2ALdm3ofbtxLh6IbV95Ro5AKWJUP",
|
||||||
|
"refresh_token":"zVtxJrKhNhBcNyqCz1NggNAAmehAxnRO3Z0fXmCp",
|
||||||
|
"expires_in":36000
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const refreshedTokenPayload = `
|
||||||
|
{
|
||||||
|
"access_token":"VDZupx0usVRV4oOd1FCu4xUxgk8SY0TK",
|
||||||
|
"refresh_token":"BenBQq7TWZ7Cp0aUM47nQjTz2QHNmTWcPctB609n",
|
||||||
|
"expires_in":36000
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const invalidRefreshTokenPayload = `
|
||||||
|
{
|
||||||
|
"code":3006,
|
||||||
|
"msg":{
|
||||||
|
"oauth_refresh_token_error":"Token校验失败"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const invalidCodePayload = `
|
||||||
|
{
|
||||||
|
"code":3003,
|
||||||
|
"msg":{
|
||||||
|
"oauth_validate_code_error":"code校验失败"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const userPayload = `
|
||||||
|
{
|
||||||
|
"code":0,
|
||||||
|
"data":{
|
||||||
|
"global_key":"demo1",
|
||||||
|
"email":"demo1@gmail.com",
|
||||||
|
"avatar":"/static/fruit_avatar/Fruit-20.png"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const userNotFoundPayload = `
|
||||||
|
{
|
||||||
|
"code":1,
|
||||||
|
"msg":{
|
||||||
|
"user_not_login":"用户未登录"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const fakeProjectPayload = `
|
||||||
|
{
|
||||||
|
"code":0,
|
||||||
|
"data":{
|
||||||
|
"owner_user_name":"demo1",
|
||||||
|
"name":"test1",
|
||||||
|
"depot_path":"/u/gilala/p/abp/git",
|
||||||
|
"https_url":"https://git.coding.net/demo1/test1.git",
|
||||||
|
"is_public": false,
|
||||||
|
"icon":"/static/project_icon/scenery-5.png",
|
||||||
|
"current_user_role":"owner"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const fakePermOwnerPayload = `
|
||||||
|
{
|
||||||
|
"code":0,
|
||||||
|
"data":{
|
||||||
|
"current_user_role":"owner"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const fakePermAdminPayload = `
|
||||||
|
{
|
||||||
|
"code":0,
|
||||||
|
"data":{
|
||||||
|
"current_user_role":"admin"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const fakePermMemberPayload = `
|
||||||
|
{
|
||||||
|
"code":0,
|
||||||
|
"data":{
|
||||||
|
"current_user_role":"member"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const fakePermGuestPayload = `
|
||||||
|
{
|
||||||
|
"code":0,
|
||||||
|
"data":{
|
||||||
|
"current_user_role":"guest"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const fakeDepotPayload = `
|
||||||
|
{
|
||||||
|
"code":0,
|
||||||
|
"data":{
|
||||||
|
"default_branch":"master"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const projectNotFoundPayload = `
|
||||||
|
{
|
||||||
|
"code":1100,
|
||||||
|
"msg":{
|
||||||
|
"project_not_exists":"项目不存在"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const fakeProjectsPayload = `
|
||||||
|
{
|
||||||
|
"code":0,
|
||||||
|
"data":{
|
||||||
|
"list":{
|
||||||
|
"owner_user_name":"demo1",
|
||||||
|
"name":"test1",
|
||||||
|
"icon":"/static/project_icon/scenery-5.png",
|
||||||
|
},
|
||||||
|
"page":1,
|
||||||
|
"pageSize":1,
|
||||||
|
"totalPage":1,
|
||||||
|
"totalRow":1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const fakeFilePayload = `
|
||||||
|
{
|
||||||
|
"code":0,
|
||||||
|
"data":{
|
||||||
|
"file":{
|
||||||
|
"data":"pipeline:\n test:\n image: golang:1.6\n commands:\n - go test\n"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const fileNotFoundPayload = `
|
||||||
|
{
|
||||||
|
"code":0,
|
||||||
|
"data":{
|
||||||
|
"ref":"master"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const fakeHooksPayload = `
|
||||||
|
{
|
||||||
|
"code":0,
|
||||||
|
"data":[
|
||||||
|
{
|
||||||
|
"id":2,
|
||||||
|
"hook_url":"http://127.0.0.2"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id":3,
|
||||||
|
"hook_url":"http://127.0.0.3"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
`
|
175
remote/coding/fixtures/hooks.go
Normal file
175
remote/coding/fixtures/hooks.go
Normal file
|
@ -0,0 +1,175 @@
|
||||||
|
package fixtures
|
||||||
|
|
||||||
|
const PushHook = `
|
||||||
|
{
|
||||||
|
"ref": "refs/heads/master",
|
||||||
|
"before": "861f2315056e8925e627a6f46518b9df05896e24",
|
||||||
|
"commits": [
|
||||||
|
{
|
||||||
|
"committer": {
|
||||||
|
"name": "demo1",
|
||||||
|
"email": "demo1@gmail.com"
|
||||||
|
},
|
||||||
|
"web_url": "https://coding.net/u/demo1/p/test1/git/commit/5b9912a6ff272e9c93a4c44c278fe9b359ed1ab4",
|
||||||
|
"short_message": "new file .drone.yml\n",
|
||||||
|
"sha": "5b9912a6ff272e9c93a4c44c278fe9b359ed1ab4"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"after": "5b9912a6ff272e9c93a4c44c278fe9b359ed1ab4",
|
||||||
|
"event": "push",
|
||||||
|
"repository": {
|
||||||
|
"owner": {
|
||||||
|
"path": "/u/demo1",
|
||||||
|
"web_url": "https://coding.net/u/demo1",
|
||||||
|
"global_key": "demo1",
|
||||||
|
"name": "demo1",
|
||||||
|
"avatar": "/static/fruit_avatar/Fruit-20.png"
|
||||||
|
},
|
||||||
|
"https_url": "https://git.coding.net/demo1/test1.git",
|
||||||
|
"web_url": "https://coding.net/u/demo1/p/test1",
|
||||||
|
"project_id": "99999999",
|
||||||
|
"ssh_url": "git@git.coding.net:demo1/test1.git",
|
||||||
|
"name": "test1",
|
||||||
|
"description": ""
|
||||||
|
},
|
||||||
|
"user": {
|
||||||
|
"path": "/u/demo1",
|
||||||
|
"web_url": "https://coding.net/u/demo1",
|
||||||
|
"global_key": "demo1",
|
||||||
|
"name": "demo1",
|
||||||
|
"avatar": "/static/fruit_avatar/Fruit-20.png"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const DeleteBranchPushHook = `
|
||||||
|
{
|
||||||
|
"ref": "refs/heads/master",
|
||||||
|
"before": "861f2315056e8925e627a6f46518b9df05896e24",
|
||||||
|
"after": "0000000000000000000000000000000000000000",
|
||||||
|
"event": "push",
|
||||||
|
"repository": {
|
||||||
|
"owner": {
|
||||||
|
"path": "/u/demo1",
|
||||||
|
"web_url": "https://coding.net/u/demo1",
|
||||||
|
"global_key": "demo1",
|
||||||
|
"name": "demo1",
|
||||||
|
"avatar": "/static/fruit_avatar/Fruit-20.png"
|
||||||
|
},
|
||||||
|
"https_url": "https://git.coding.net/demo1/test1.git",
|
||||||
|
"web_url": "https://coding.net/u/demo1/p/test1",
|
||||||
|
"project_id": "99999999",
|
||||||
|
"ssh_url": "git@git.coding.net:demo1/test1.git",
|
||||||
|
"name": "test1",
|
||||||
|
"description": ""
|
||||||
|
},
|
||||||
|
"user": {
|
||||||
|
"path": "/u/demo1",
|
||||||
|
"web_url": "https://coding.net/u/demo1",
|
||||||
|
"global_key": "demo1",
|
||||||
|
"name": "demo1",
|
||||||
|
"avatar": "/static/fruit_avatar/Fruit-20.png"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const PullRequestHook = `
|
||||||
|
{
|
||||||
|
"pull_request": {
|
||||||
|
"target_branch": "master",
|
||||||
|
"title": "pr1",
|
||||||
|
"body": "pr message",
|
||||||
|
"source_sha": "",
|
||||||
|
"source_repository": {
|
||||||
|
"owner": {
|
||||||
|
"path": "/u/demo2",
|
||||||
|
"web_url": "https://coding.net/u/demo2",
|
||||||
|
"global_key": "demo2",
|
||||||
|
"name": "demo2",
|
||||||
|
"avatar": "/static/fruit_avatar/Fruit-2.png"
|
||||||
|
},
|
||||||
|
"https_url": "https://git.coding.net/demo2/test2.git",
|
||||||
|
"web_url": "https://coding.net/u/demo2/p/test2",
|
||||||
|
"project_id": "7777777",
|
||||||
|
"ssh_url": "git@git.coding.net:demo2/test2.git",
|
||||||
|
"name": "test2",
|
||||||
|
"description": "",
|
||||||
|
"git_url": "git://git.coding.net/demo2/test2.git"
|
||||||
|
},
|
||||||
|
"source_branch": "master",
|
||||||
|
"number": 1,
|
||||||
|
"web_url": "https://coding.net/u/demo1/p/test2/git/pull/1",
|
||||||
|
"merge_commit_sha": "55e77b328b71d3ee4f9e70a5f67231b0acceeadc",
|
||||||
|
"target_sha": "",
|
||||||
|
"action": "create",
|
||||||
|
"id": 7586,
|
||||||
|
"user": {
|
||||||
|
"path": "/u/demo2",
|
||||||
|
"web_url": "https://coding.net/u/demo2",
|
||||||
|
"global_key": "demo2",
|
||||||
|
"name": "demo2",
|
||||||
|
"avatar": "/static/fruit_avatar/Fruit-2.png"
|
||||||
|
},
|
||||||
|
"status": "CANMERGE"
|
||||||
|
},
|
||||||
|
"repository": {
|
||||||
|
"owner": {
|
||||||
|
"path": "/u/demo1",
|
||||||
|
"web_url": "https://coding.net/u/demo1",
|
||||||
|
"global_key": "demo1",
|
||||||
|
"name": "demo1",
|
||||||
|
"avatar": "/static/fruit_avatar/Fruit-20.png"
|
||||||
|
},
|
||||||
|
"https_url": "https://git.coding.net/demo1/test2.git",
|
||||||
|
"web_url": "https://coding.net/u/demo1/p/test2",
|
||||||
|
"project_id": "6666666",
|
||||||
|
"ssh_url": "git@git.coding.net:demo1/test2.git",
|
||||||
|
"name": "test2",
|
||||||
|
"description": "",
|
||||||
|
"git_url": "git://git.coding.net/demo1/test2.git"
|
||||||
|
},
|
||||||
|
"event": "pull_request"
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const MergeRequestHook = `
|
||||||
|
{
|
||||||
|
"merge_request": {
|
||||||
|
"target_branch": "master",
|
||||||
|
"title": "mr1",
|
||||||
|
"body": "<p>mr message</p>",
|
||||||
|
"source_sha": "",
|
||||||
|
"source_branch": "branch1",
|
||||||
|
"number": 1,
|
||||||
|
"web_url": "https://coding.net/u/demo1/p/test1/git/merge/1",
|
||||||
|
"merge_commit_sha": "74e6755580c34e9fd81dbcfcbd43ee5f30259436",
|
||||||
|
"target_sha": "",
|
||||||
|
"action": "create",
|
||||||
|
"id": 533428,
|
||||||
|
"user": {
|
||||||
|
"path": "/u/demo1",
|
||||||
|
"web_url": "https://coding.net/u/demo1",
|
||||||
|
"global_key": "demo1",
|
||||||
|
"name": "demo1",
|
||||||
|
"avatar": "/static/fruit_avatar/Fruit-20.png"
|
||||||
|
},
|
||||||
|
"status": "CANMERGE"
|
||||||
|
},
|
||||||
|
"repository": {
|
||||||
|
"owner": {
|
||||||
|
"path": "/u/demo1",
|
||||||
|
"web_url": "https://coding.net/u/demo1",
|
||||||
|
"global_key": "demo1",
|
||||||
|
"name": "demo1",
|
||||||
|
"avatar": "/static/fruit_avatar/Fruit-20.png"
|
||||||
|
},
|
||||||
|
"https_url": "https://git.coding.net/demo1/test1.git",
|
||||||
|
"web_url": "https://coding.net/u/demo1/p/test1",
|
||||||
|
"project_id": "99999999",
|
||||||
|
"ssh_url": "git@git.coding.net:demo1/test1.git",
|
||||||
|
"name": "test1",
|
||||||
|
"description": ""
|
||||||
|
},
|
||||||
|
"event": "merge_request"
|
||||||
|
}
|
||||||
|
`
|
229
remote/coding/hook.go
Normal file
229
remote/coding/hook.go
Normal file
|
@ -0,0 +1,229 @@
|
||||||
|
package coding
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/drone/drone/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
hookEvent = "X-Coding-Event"
|
||||||
|
hookPush = "push"
|
||||||
|
hookPR = "pull_request"
|
||||||
|
hookMR = "merge_request"
|
||||||
|
)
|
||||||
|
|
||||||
|
type User struct {
|
||||||
|
GlobalKey string `json:"global_key"`
|
||||||
|
Avatar string `json:"avatar"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Repository struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
HttpsURL string `json:"https_url"`
|
||||||
|
SshURL string `json:"ssh_url"`
|
||||||
|
WebURL string `json:"web_url"`
|
||||||
|
Owner *User `json:"owner"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Committer struct {
|
||||||
|
Email string `json:"email"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Commit struct {
|
||||||
|
SHA string `json:"sha"`
|
||||||
|
ShortMessage string `json:"short_message"`
|
||||||
|
Committer *Committer `json:"committer"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type PullRequest MergeRequest
|
||||||
|
|
||||||
|
type MergeRequest struct {
|
||||||
|
SourceBranch string `json:"source_branch"`
|
||||||
|
TargetBranch string `json:"target_branch"`
|
||||||
|
CommitSHA string `json:"merge_commit_sha"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
Action string `json:"action"`
|
||||||
|
Number float64 `json:"number"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
Body string `json:"body"`
|
||||||
|
WebURL string `json:"web_url"`
|
||||||
|
User *User `json:"user"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type PushHook struct {
|
||||||
|
Event string `json:"event"`
|
||||||
|
Repository *Repository `json:"repository"`
|
||||||
|
Ref string `json:"ref"`
|
||||||
|
Before string `json:"before"`
|
||||||
|
After string `json:"after"`
|
||||||
|
Commits []*Commit `json:"commits"`
|
||||||
|
User *User `json:"user"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type PullRequestHook struct {
|
||||||
|
Event string `json:"event"`
|
||||||
|
Repository *Repository `json:"repository"`
|
||||||
|
PullRequest *PullRequest `json:"pull_request"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type MergeRequestHook struct {
|
||||||
|
Event string `json:"event"`
|
||||||
|
Repository *Repository `json:"repository"`
|
||||||
|
MergeRequest *MergeRequest `json:"merge_request"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseHook(r *http.Request) (*model.Repo, *model.Build, error) {
|
||||||
|
raw, err := ioutil.ReadAll(r.Body)
|
||||||
|
defer r.Body.Close()
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
switch r.Header.Get(hookEvent) {
|
||||||
|
case hookPush:
|
||||||
|
return parsePushHook(raw)
|
||||||
|
case hookPR:
|
||||||
|
return parsePullRequestHook(raw)
|
||||||
|
case hookMR:
|
||||||
|
return parseMergeReuqestHook(raw)
|
||||||
|
}
|
||||||
|
return nil, nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func findLastCommit(commits []*Commit, sha string) *Commit {
|
||||||
|
var lastCommit *Commit
|
||||||
|
for _, commit := range commits {
|
||||||
|
if commit.SHA == sha {
|
||||||
|
lastCommit = commit
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if lastCommit == nil {
|
||||||
|
lastCommit = &Commit{}
|
||||||
|
}
|
||||||
|
if lastCommit.Committer == nil {
|
||||||
|
lastCommit.Committer = &Committer{}
|
||||||
|
}
|
||||||
|
return lastCommit
|
||||||
|
}
|
||||||
|
|
||||||
|
func convertRepository(repo *Repository) (*model.Repo, error) {
|
||||||
|
// tricky stuff for a team project without a team owner instead of a user owner
|
||||||
|
re := regexp.MustCompile(`git@.+:([^/]+)/.+\.git`)
|
||||||
|
matches := re.FindStringSubmatch(repo.SshURL)
|
||||||
|
if len(matches) != 2 {
|
||||||
|
return nil, fmt.Errorf("Unable to resolve owner from ssh url %q", repo.SshURL)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &model.Repo{
|
||||||
|
Owner: matches[1],
|
||||||
|
Name: repo.Name,
|
||||||
|
FullName: projectFullName(repo.Owner.GlobalKey, repo.Name),
|
||||||
|
Link: repo.WebURL,
|
||||||
|
Kind: model.RepoGit,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parsePushHook(raw []byte) (*model.Repo, *model.Build, error) {
|
||||||
|
hook := &PushHook{}
|
||||||
|
err := json.Unmarshal(raw, hook)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// no build triggered when removing ref
|
||||||
|
if hook.After == "0000000000000000000000000000000000000000" {
|
||||||
|
return nil, nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
repo, err := convertRepository(hook.Repository)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
lastCommit := findLastCommit(hook.Commits, hook.After)
|
||||||
|
build := &model.Build{
|
||||||
|
Event: model.EventPush,
|
||||||
|
Commit: hook.After,
|
||||||
|
Ref: hook.Ref,
|
||||||
|
Link: fmt.Sprintf("%s/git/commit/%s", hook.Repository.WebURL, hook.After),
|
||||||
|
Branch: strings.Replace(hook.Ref, "refs/heads/", "", -1),
|
||||||
|
Message: lastCommit.ShortMessage,
|
||||||
|
Email: lastCommit.Committer.Email,
|
||||||
|
Avatar: hook.User.Avatar,
|
||||||
|
Author: hook.User.GlobalKey,
|
||||||
|
Remote: hook.Repository.HttpsURL,
|
||||||
|
}
|
||||||
|
return repo, build, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parsePullRequestHook(raw []byte) (*model.Repo, *model.Build, error) {
|
||||||
|
hook := &PullRequestHook{}
|
||||||
|
err := json.Unmarshal(raw, hook)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
if hook.PullRequest.Status != "CANMERGE" ||
|
||||||
|
(hook.PullRequest.Action != "create" && hook.PullRequest.Action != "synchronize") {
|
||||||
|
return nil, nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
repo, err := convertRepository(hook.Repository)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
build := &model.Build{
|
||||||
|
Event: model.EventPull,
|
||||||
|
Commit: hook.PullRequest.CommitSHA,
|
||||||
|
Link: hook.PullRequest.WebURL,
|
||||||
|
Ref: fmt.Sprintf("refs/pull/%d/MERGE", int(hook.PullRequest.Number)),
|
||||||
|
Branch: hook.PullRequest.TargetBranch,
|
||||||
|
Message: hook.PullRequest.Body,
|
||||||
|
Author: hook.PullRequest.User.GlobalKey,
|
||||||
|
Avatar: hook.PullRequest.User.Avatar,
|
||||||
|
Title: hook.PullRequest.Title,
|
||||||
|
Remote: hook.Repository.HttpsURL,
|
||||||
|
Refspec: fmt.Sprintf("%s:%s", hook.PullRequest.SourceBranch, hook.PullRequest.TargetBranch),
|
||||||
|
}
|
||||||
|
|
||||||
|
return repo, build, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseMergeReuqestHook(raw []byte) (*model.Repo, *model.Build, error) {
|
||||||
|
hook := &MergeRequestHook{}
|
||||||
|
err := json.Unmarshal(raw, hook)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
if hook.MergeRequest.Status != "CANMERGE" ||
|
||||||
|
(hook.MergeRequest.Action != "create" && hook.MergeRequest.Action != "synchronize") {
|
||||||
|
return nil, nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
repo, err := convertRepository(hook.Repository)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
build := &model.Build{
|
||||||
|
Event: model.EventPull,
|
||||||
|
Commit: hook.MergeRequest.CommitSHA,
|
||||||
|
Link: hook.MergeRequest.WebURL,
|
||||||
|
Ref: fmt.Sprintf("refs/merge/%d/MERGE", int(hook.MergeRequest.Number)),
|
||||||
|
Branch: hook.MergeRequest.TargetBranch,
|
||||||
|
Message: hook.MergeRequest.Body,
|
||||||
|
Author: hook.MergeRequest.User.GlobalKey,
|
||||||
|
Avatar: hook.MergeRequest.User.Avatar,
|
||||||
|
Title: hook.MergeRequest.Title,
|
||||||
|
Remote: hook.Repository.HttpsURL,
|
||||||
|
Refspec: fmt.Sprintf("%s:%s", hook.MergeRequest.SourceBranch, hook.MergeRequest.TargetBranch),
|
||||||
|
}
|
||||||
|
return repo, build, nil
|
||||||
|
}
|
192
remote/coding/hook_test.go
Normal file
192
remote/coding/hook_test.go
Normal file
|
@ -0,0 +1,192 @@
|
||||||
|
package coding
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/drone/drone/model"
|
||||||
|
"github.com/drone/drone/remote/coding/fixtures"
|
||||||
|
|
||||||
|
"github.com/franela/goblin"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Test_hook(t *testing.T) {
|
||||||
|
|
||||||
|
g := goblin.Goblin(t)
|
||||||
|
g.Describe("Coding hook", func() {
|
||||||
|
|
||||||
|
g.It("Should parse hook", func() {
|
||||||
|
|
||||||
|
reader := ioutil.NopCloser(strings.NewReader(fixtures.PushHook))
|
||||||
|
r := &http.Request{
|
||||||
|
Header: map[string][]string{
|
||||||
|
hookEvent: {hookPush},
|
||||||
|
},
|
||||||
|
Body: reader,
|
||||||
|
}
|
||||||
|
|
||||||
|
repo := &model.Repo{
|
||||||
|
Owner: "demo1",
|
||||||
|
Name: "test1",
|
||||||
|
FullName: "demo1/test1",
|
||||||
|
Link: "https://coding.net/u/demo1/p/test1",
|
||||||
|
Kind: model.RepoGit,
|
||||||
|
}
|
||||||
|
|
||||||
|
build := &model.Build{
|
||||||
|
Event: model.EventPush,
|
||||||
|
Commit: "5b9912a6ff272e9c93a4c44c278fe9b359ed1ab4",
|
||||||
|
Ref: "refs/heads/master",
|
||||||
|
Link: "https://coding.net/u/demo1/p/test1/git/commit/5b9912a6ff272e9c93a4c44c278fe9b359ed1ab4",
|
||||||
|
Branch: "master",
|
||||||
|
Message: "new file .drone.yml\n",
|
||||||
|
Email: "demo1@gmail.com",
|
||||||
|
Avatar: "/static/fruit_avatar/Fruit-20.png",
|
||||||
|
Author: "demo1",
|
||||||
|
Remote: "https://git.coding.net/demo1/test1.git",
|
||||||
|
}
|
||||||
|
|
||||||
|
actualRepo, actualBuild, err := parseHook(r)
|
||||||
|
g.Assert(err == nil).IsTrue()
|
||||||
|
g.Assert(actualRepo).Equal(repo)
|
||||||
|
g.Assert(actualBuild).Equal(build)
|
||||||
|
})
|
||||||
|
|
||||||
|
g.It("Should find last commit", func() {
|
||||||
|
commit1 := &Commit{SHA: "1234567890", Committer: &Committer{}}
|
||||||
|
commit2 := &Commit{SHA: "abcdef1234", Committer: &Committer{}}
|
||||||
|
commits := []*Commit{commit1, commit2}
|
||||||
|
g.Assert(findLastCommit(commits, "abcdef1234")).Equal(commit2)
|
||||||
|
})
|
||||||
|
|
||||||
|
g.It("Should find last commit", func() {
|
||||||
|
commit1 := &Commit{SHA: "1234567890", Committer: &Committer{}}
|
||||||
|
commit2 := &Commit{SHA: "abcdef1234", Committer: &Committer{}}
|
||||||
|
commits := []*Commit{commit1, commit2}
|
||||||
|
emptyCommit := &Commit{Committer: &Committer{}}
|
||||||
|
g.Assert(findLastCommit(commits, "00000000000")).Equal(emptyCommit)
|
||||||
|
})
|
||||||
|
|
||||||
|
g.It("Should convert repository", func() {
|
||||||
|
repository := &Repository{
|
||||||
|
Name: "test_project",
|
||||||
|
HttpsURL: "https://git.coding.net/kelvin/test_project.git",
|
||||||
|
SshURL: "git@git.coding.net:kelvin/test_project.git",
|
||||||
|
WebURL: "https://coding.net/u/kelvin/p/test_project",
|
||||||
|
Owner: &User{
|
||||||
|
GlobalKey: "kelvin",
|
||||||
|
Avatar: "https://dn-coding-net-production-static.qbox.me/9ed11de3-65e3-4cd8-b6aa-5abe7285ab43.jpeg?imageMogr2/auto-orient/format/jpeg/crop/!209x209a0a0",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
repo := &model.Repo{
|
||||||
|
Owner: "kelvin",
|
||||||
|
Name: "test_project",
|
||||||
|
FullName: "kelvin/test_project",
|
||||||
|
Link: "https://coding.net/u/kelvin/p/test_project",
|
||||||
|
Kind: model.RepoGit,
|
||||||
|
}
|
||||||
|
actual, err := convertRepository(repository)
|
||||||
|
g.Assert(err == nil).IsTrue()
|
||||||
|
g.Assert(actual).Equal(repo)
|
||||||
|
})
|
||||||
|
|
||||||
|
g.It("Should parse push hook", func() {
|
||||||
|
|
||||||
|
repo := &model.Repo{
|
||||||
|
Owner: "demo1",
|
||||||
|
Name: "test1",
|
||||||
|
FullName: "demo1/test1",
|
||||||
|
Link: "https://coding.net/u/demo1/p/test1",
|
||||||
|
Kind: model.RepoGit,
|
||||||
|
}
|
||||||
|
|
||||||
|
build := &model.Build{
|
||||||
|
Event: model.EventPush,
|
||||||
|
Commit: "5b9912a6ff272e9c93a4c44c278fe9b359ed1ab4",
|
||||||
|
Ref: "refs/heads/master",
|
||||||
|
Link: "https://coding.net/u/demo1/p/test1/git/commit/5b9912a6ff272e9c93a4c44c278fe9b359ed1ab4",
|
||||||
|
Branch: "master",
|
||||||
|
Message: "new file .drone.yml\n",
|
||||||
|
Email: "demo1@gmail.com",
|
||||||
|
Avatar: "/static/fruit_avatar/Fruit-20.png",
|
||||||
|
Author: "demo1",
|
||||||
|
Remote: "https://git.coding.net/demo1/test1.git",
|
||||||
|
}
|
||||||
|
|
||||||
|
actualRepo, actualBuild, err := parsePushHook([]byte(fixtures.PushHook))
|
||||||
|
g.Assert(err == nil).IsTrue()
|
||||||
|
g.Assert(actualRepo).Equal(repo)
|
||||||
|
g.Assert(actualBuild).Equal(build)
|
||||||
|
})
|
||||||
|
|
||||||
|
g.It("Should parse delete branch push hook", func() {
|
||||||
|
actualRepo, actualBuild, err := parsePushHook([]byte(fixtures.DeleteBranchPushHook))
|
||||||
|
g.Assert(err == nil).IsTrue()
|
||||||
|
g.Assert(actualRepo == nil).IsTrue()
|
||||||
|
g.Assert(actualBuild == nil).IsTrue()
|
||||||
|
})
|
||||||
|
|
||||||
|
g.It("Should parse pull request hook", func() {
|
||||||
|
|
||||||
|
repo := &model.Repo{
|
||||||
|
Owner: "demo1",
|
||||||
|
Name: "test2",
|
||||||
|
FullName: "demo1/test2",
|
||||||
|
Link: "https://coding.net/u/demo1/p/test2",
|
||||||
|
Kind: model.RepoGit,
|
||||||
|
}
|
||||||
|
|
||||||
|
build := &model.Build{
|
||||||
|
Event: model.EventPull,
|
||||||
|
Commit: "55e77b328b71d3ee4f9e70a5f67231b0acceeadc",
|
||||||
|
Link: "https://coding.net/u/demo1/p/test2/git/pull/1",
|
||||||
|
Ref: "refs/pull/1/MERGE",
|
||||||
|
Branch: "master",
|
||||||
|
Message: "pr message",
|
||||||
|
Author: "demo2",
|
||||||
|
Avatar: "/static/fruit_avatar/Fruit-2.png",
|
||||||
|
Title: "pr1",
|
||||||
|
Remote: "https://git.coding.net/demo1/test2.git",
|
||||||
|
Refspec: "master:master",
|
||||||
|
}
|
||||||
|
|
||||||
|
actualRepo, actualBuild, err := parsePullRequestHook([]byte(fixtures.PullRequestHook))
|
||||||
|
g.Assert(err == nil).IsTrue()
|
||||||
|
g.Assert(actualRepo).Equal(repo)
|
||||||
|
g.Assert(actualBuild).Equal(build)
|
||||||
|
})
|
||||||
|
|
||||||
|
g.It("Should parse merge request hook", func() {
|
||||||
|
|
||||||
|
repo := &model.Repo{
|
||||||
|
Owner: "demo1",
|
||||||
|
Name: "test1",
|
||||||
|
FullName: "demo1/test1",
|
||||||
|
Link: "https://coding.net/u/demo1/p/test1",
|
||||||
|
Kind: model.RepoGit,
|
||||||
|
}
|
||||||
|
|
||||||
|
build := &model.Build{
|
||||||
|
Event: model.EventPull,
|
||||||
|
Commit: "74e6755580c34e9fd81dbcfcbd43ee5f30259436",
|
||||||
|
Link: "https://coding.net/u/demo1/p/test1/git/merge/1",
|
||||||
|
Ref: "refs/merge/1/MERGE",
|
||||||
|
Branch: "master",
|
||||||
|
Message: "<p>mr message</p>",
|
||||||
|
Author: "demo1",
|
||||||
|
Avatar: "/static/fruit_avatar/Fruit-20.png",
|
||||||
|
Title: "mr1",
|
||||||
|
Remote: "https://git.coding.net/demo1/test1.git",
|
||||||
|
Refspec: "branch1:master",
|
||||||
|
}
|
||||||
|
|
||||||
|
actualRepo, actualBuild, err := parseMergeReuqestHook([]byte(fixtures.MergeRequestHook))
|
||||||
|
g.Assert(err == nil).IsTrue()
|
||||||
|
g.Assert(actualRepo).Equal(repo)
|
||||||
|
g.Assert(actualBuild).Equal(build)
|
||||||
|
})
|
||||||
|
|
||||||
|
})
|
||||||
|
}
|
84
remote/coding/internal/coding.go
Normal file
84
remote/coding/internal/coding.go
Normal file
|
@ -0,0 +1,84 @@
|
||||||
|
package internal
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Client struct {
|
||||||
|
baseURL string
|
||||||
|
apiPath string
|
||||||
|
token string
|
||||||
|
agent string
|
||||||
|
client *http.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
type GenericAPIResponse struct {
|
||||||
|
Code int `json:"code"`
|
||||||
|
Data json.RawMessage `json:"data,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewClient(baseURL, apiPath, token, agent string, client *http.Client) *Client {
|
||||||
|
return &Client{
|
||||||
|
baseURL: baseURL,
|
||||||
|
apiPath: apiPath,
|
||||||
|
token: token,
|
||||||
|
agent: agent,
|
||||||
|
client: client,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generic GET for requesting Coding OAuth API
|
||||||
|
func (c *Client) Get(u string, params url.Values) ([]byte, error) {
|
||||||
|
return c.Do(http.MethodGet, u, params)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generic method for requesting Coding OAuth API
|
||||||
|
func (c *Client) Do(method, u string, params url.Values) ([]byte, error) {
|
||||||
|
if params == nil {
|
||||||
|
params = url.Values{}
|
||||||
|
}
|
||||||
|
params.Set("access_token", c.token)
|
||||||
|
|
||||||
|
rawURL := c.baseURL + c.apiPath + u
|
||||||
|
|
||||||
|
var req *http.Request
|
||||||
|
var err error
|
||||||
|
if method != "GET" {
|
||||||
|
req, err = http.NewRequest(method, rawURL+"?access_token="+c.token, strings.NewReader(params.Encode()))
|
||||||
|
req.Header.Set("Content-Type", "application/x-www-form-urlencoded;charset=UTF-8")
|
||||||
|
} else {
|
||||||
|
req, err = http.NewRequest("GET", rawURL+"?"+params.Encode(), nil)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("fail to create request for url %q: %v", rawURL, err)
|
||||||
|
}
|
||||||
|
req.Header.Set("User-Agent", c.agent)
|
||||||
|
|
||||||
|
resp, err := c.client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("fail to request %s %s: %v", req.Method, req.URL, err)
|
||||||
|
}
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return nil, fmt.Errorf("%s %s respond %d", req.Method, req.URL, resp.StatusCode)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
body, err := ioutil.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("fail to read response from %s %s: %v", req.Method, req.URL.String(), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
apiResp := &GenericAPIResponse{}
|
||||||
|
err = json.Unmarshal(body, apiResp)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("fail to parse response from %s %s: %v", req.Method, req.URL.String(), err)
|
||||||
|
}
|
||||||
|
if apiResp.Code != 0 {
|
||||||
|
return nil, fmt.Errorf("Coding OAuth API respond error: %s", string(body))
|
||||||
|
}
|
||||||
|
return apiResp.Data, nil
|
||||||
|
}
|
15
remote/coding/internal/error.go
Normal file
15
remote/coding/internal/error.go
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
package internal
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
type APIClientErr struct {
|
||||||
|
Message string
|
||||||
|
URL string
|
||||||
|
Cause error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e APIClientErr) Error() string {
|
||||||
|
return fmt.Sprintf("%s (Requested %s): %v", e.Message, e.URL, e.Cause)
|
||||||
|
}
|
31
remote/coding/internal/file.go
Normal file
31
remote/coding/internal/file.go
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
package internal
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Commit struct {
|
||||||
|
File *File `json:"file"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type File struct {
|
||||||
|
Data string `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) GetFile(globalKey, projectName, ref, path string) ([]byte, error) {
|
||||||
|
u := fmt.Sprintf("/user/%s/project/%s/git/blob/%s/%s", globalKey, projectName, ref, path)
|
||||||
|
resp, err := c.Get(u, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
commit := &Commit{}
|
||||||
|
err = json.Unmarshal(resp, commit)
|
||||||
|
if err != nil {
|
||||||
|
return nil, APIClientErr{"fail to parse file data", u, err}
|
||||||
|
}
|
||||||
|
if commit == nil || commit.File == nil {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return []byte(commit.File.Data), nil
|
||||||
|
}
|
93
remote/coding/internal/project.go
Normal file
93
remote/coding/internal/project.go
Normal file
|
@ -0,0 +1,93 @@
|
||||||
|
package internal
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/url"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Project struct {
|
||||||
|
Owner string `json:"owner_user_name"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
DepotPath string `json:"depot_path"`
|
||||||
|
HttpsURL string `json:"https_url"`
|
||||||
|
IsPublic bool `json:"is_public"`
|
||||||
|
Icon string `json:"icon"`
|
||||||
|
Role string `json:"current_user_role"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Depot struct {
|
||||||
|
DefaultBranch string `json:"default_branch"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ProjectListData struct {
|
||||||
|
Page int `json:"page"`
|
||||||
|
PageSize int `json:"pageSize"`
|
||||||
|
TotalPage int `json:"totalPage"`
|
||||||
|
TotalRow int `json:"totalRow"`
|
||||||
|
List []*Project `json:"list"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) GetProject(globalKey, projectName string) (*Project, error) {
|
||||||
|
u := fmt.Sprintf("/user/%s/project/%s", globalKey, projectName)
|
||||||
|
resp, err := c.Get(u, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
project := &Project{}
|
||||||
|
err = json.Unmarshal(resp, project)
|
||||||
|
if err != nil {
|
||||||
|
return nil, APIClientErr{"fail to parse project data", u, err}
|
||||||
|
}
|
||||||
|
return project, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) GetDepot(globalKey, projectName string) (*Depot, error) {
|
||||||
|
u := fmt.Sprintf("/user/%s/project/%s/git", globalKey, projectName)
|
||||||
|
resp, err := c.Get(u, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
depot := &Depot{}
|
||||||
|
err = json.Unmarshal(resp, depot)
|
||||||
|
if err != nil {
|
||||||
|
return nil, APIClientErr{"fail to parse depot data", u, err}
|
||||||
|
}
|
||||||
|
return depot, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) GetProjectList() ([]*Project, error) {
|
||||||
|
u := "/user/projects"
|
||||||
|
resp, err := c.Get(u, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
data := &ProjectListData{}
|
||||||
|
err = json.Unmarshal(resp, data)
|
||||||
|
if err != nil {
|
||||||
|
return nil, APIClientErr{"fail to parse project list data", u, err}
|
||||||
|
}
|
||||||
|
if data.TotalPage == 1 {
|
||||||
|
return data.List, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
projectList := make([]*Project, 0)
|
||||||
|
projectList = append(projectList, data.List...)
|
||||||
|
for i := 2; i <= data.TotalPage; i++ {
|
||||||
|
params := url.Values{}
|
||||||
|
params.Set("page", fmt.Sprintf("%d", i))
|
||||||
|
resp, err := c.Get(u, params)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
data := &ProjectListData{}
|
||||||
|
err = json.Unmarshal(resp, data)
|
||||||
|
if err != nil {
|
||||||
|
return nil, APIClientErr{"fail to parse project list data", u, err}
|
||||||
|
}
|
||||||
|
projectList = append(projectList, data.List...)
|
||||||
|
}
|
||||||
|
return projectList, nil
|
||||||
|
}
|
25
remote/coding/internal/user.go
Normal file
25
remote/coding/internal/user.go
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
package internal
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
)
|
||||||
|
|
||||||
|
type User struct {
|
||||||
|
GlobalKey string `json:"global_key"`
|
||||||
|
Email string `json:"email"`
|
||||||
|
Avatar string `json:"avatar"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) GetCurrentUser() (*User, error) {
|
||||||
|
u := "/account/current_user"
|
||||||
|
resp, err := c.Get(u, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
user := &User{}
|
||||||
|
err = json.Unmarshal(resp, user)
|
||||||
|
if err != nil {
|
||||||
|
return nil, APIClientErr{"fail to parse current user data", u, err}
|
||||||
|
}
|
||||||
|
return user, nil
|
||||||
|
}
|
92
remote/coding/internal/webhook.go
Normal file
92
remote/coding/internal/webhook.go
Normal file
|
@ -0,0 +1,92 @@
|
||||||
|
package internal
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/url"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Webhook struct {
|
||||||
|
Id int `json:"id"`
|
||||||
|
HookURL string `json:"hook_url"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) GetWebhooks(globalKey, projectName string) ([]*Webhook, error) {
|
||||||
|
u := fmt.Sprintf("/user/%s/project/%s/git/hooks", globalKey, projectName)
|
||||||
|
resp, err := c.Get(u, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
webhooks := make([]*Webhook, 0)
|
||||||
|
err = json.Unmarshal(resp, &webhooks)
|
||||||
|
if err != nil {
|
||||||
|
return nil, APIClientErr{"fail to parse webhooks data", u, err}
|
||||||
|
}
|
||||||
|
return webhooks, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) AddWebhook(globalKey, projectName, link string) error {
|
||||||
|
webhooks, err := c.GetWebhooks(globalKey, projectName)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
webhook := matchingHooks(webhooks, link)
|
||||||
|
if webhook != nil {
|
||||||
|
u := fmt.Sprintf("/user/%s/project/%s/git/hook/%d", globalKey, projectName, webhook.Id)
|
||||||
|
params := url.Values{}
|
||||||
|
params.Set("hook_url", link)
|
||||||
|
params.Set("type_pust", "true")
|
||||||
|
params.Set("type_mr_pr", "true")
|
||||||
|
|
||||||
|
_, err := c.Do("PUT", u, params)
|
||||||
|
if err != nil {
|
||||||
|
return APIClientErr{"fail to edit webhook", u, err}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
u := fmt.Sprintf("/user/%s/project/%s/git/hook", globalKey, projectName)
|
||||||
|
params := url.Values{}
|
||||||
|
params.Set("hook_url", link)
|
||||||
|
params.Set("type_push", "true")
|
||||||
|
params.Set("type_mr_pr", "true")
|
||||||
|
|
||||||
|
_, err = c.Do("POST", u, params)
|
||||||
|
if err != nil {
|
||||||
|
return APIClientErr{"fail to add webhook", u, err}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) RemoveWebhook(globalKey, projectName, link string) error {
|
||||||
|
webhooks, err := c.GetWebhooks(globalKey, projectName)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
webhook := matchingHooks(webhooks, link)
|
||||||
|
if webhook == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
u := fmt.Sprintf("/user/%s/project/%s/git/hook/%d", globalKey, projectName, webhook.Id)
|
||||||
|
_, err = c.Do("DELETE", u, nil)
|
||||||
|
if err != nil {
|
||||||
|
return APIClientErr{"fail to remove webhook", u, err}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// helper function to return matching hook.
|
||||||
|
func matchingHooks(hooks []*Webhook, rawurl string) *Webhook {
|
||||||
|
link, err := url.Parse(rawurl)
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
for _, hook := range hooks {
|
||||||
|
hookurl, err := url.Parse(hook.HookURL)
|
||||||
|
if err == nil && hookurl.Host == link.Host {
|
||||||
|
return hook
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
9
remote/coding/util.go
Normal file
9
remote/coding/util.go
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
package coding
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
func projectFullName(owner, name string) string {
|
||||||
|
return fmt.Sprintf("%s/%s", owner, name)
|
||||||
|
}
|
18
remote/coding/util_test.go
Normal file
18
remote/coding/util_test.go
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
package coding
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/franela/goblin"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Test_util(t *testing.T) {
|
||||||
|
|
||||||
|
g := goblin.Goblin(t)
|
||||||
|
g.Describe("Coding util", func() {
|
||||||
|
|
||||||
|
g.It("Should form project full name", func() {
|
||||||
|
g.Assert(projectFullName("gk", "prj")).Equal("gk/prj")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
Loading…
Reference in a new issue