Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add gitea webhook support #81

Open
wants to merge 2 commits into
base: v5
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
202 changes: 202 additions & 0 deletions gitea/gitea.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
package gitea

import (
"encoding/json"
"errors"
"fmt"
"io"
"io/ioutil"
"net/http"

"code.gitea.io/gitea/modules/structs"
)

// parse errors
var (
ErrEventNotSpecifiedToParse = errors.New("no Event specified to parse")
ErrInvalidHTTPMethod = errors.New("invalid HTTP Method")
ErrMissingGiteaEventHeader = errors.New("missing X-Gitea-Event Header")
ErrEventNotFound = errors.New("event not defined to be parsed")
ErrParsingPayload = errors.New("error parsing payload")
ErrSecretNotMatch = errors.New("secret not match")
)

// Option is a configuration option for the webhook
type Option func(*Webhook) error

// Options is a namespace var for configuration options
var Options = WebhookOptions{}

// WebhookOptions is a namespace for configuration option methods
type WebhookOptions struct{}

// Secret registers the GitLab secret
func (WebhookOptions) Secret(secret string) Option {
return func(hook *Webhook) error {
hook.secret = secret
return nil
}
}

// Webhook instance contains all methods needed to process events
type Webhook struct {
secret string
}

// Event defines a Gitea hook event type
type Event string

// Gitea hook types
const (
CreateEvent Event = "create"
DeleteEvent Event = "delete"
ForkEvent Event = "fork"
PushEvent Event = "push"
IssuesEvent Event = "issues"
IssueCommentEvent Event = "issue_comment"
PullRequestEvent Event = "pull_request"
ReleaseEvent Event = "release"
)

// New creates and returns a WebHook instance denoted by the Provider type
func New(options ...Option) (*Webhook, error) {
hook := new(Webhook)
for _, opt := range options {
if err := opt(hook); err != nil {
return nil, errors.New("Error applying Option")
}
}
return hook, nil
}

func (hook Webhook) verifySecret(secret string) error {
if len(hook.secret) > 0 && hook.secret != secret {
return ErrSecretNotMatch
}

return nil
}

// Parse verifies and parses the events specified and returns the payload object or an error
func (hook Webhook) Parse(r *http.Request, events ...Event) (interface{}, error) {
defer func() {
_, _ = io.Copy(ioutil.Discard, r.Body)
_ = r.Body.Close()
}()

if len(events) == 0 {
return nil, ErrEventNotSpecifiedToParse
}
if r.Method != http.MethodPost {
return nil, ErrInvalidHTTPMethod
}

event := r.Header.Get("X-Gitea-Event")
if len(event) == 0 {
return nil, ErrMissingGiteaEventHeader
}

giteaEvent := Event(event)

var found bool
for _, evt := range events {
if evt == giteaEvent {
found = true
break
}
}
// event not defined to be parsed
if !found {
return nil, ErrEventNotFound
}

payload, err := ioutil.ReadAll(r.Body)
if err != nil || len(payload) == 0 {
return nil, ErrParsingPayload
}

switch giteaEvent {
case CreateEvent:
var pl structs.CreatePayload
err = json.Unmarshal([]byte(payload), &pl)

if err == nil {
err = hook.verifySecret(pl.Secret)
}

return pl, err

case ReleaseEvent:
var pl structs.ReleasePayload
err = json.Unmarshal([]byte(payload), &pl)

if err == nil {
err = hook.verifySecret(pl.Secret)
}

return pl, err

case PushEvent:
var pl structs.PushPayload
err = json.Unmarshal([]byte(payload), &pl)

if err == nil {
err = hook.verifySecret(pl.Secret)
}

return pl, err

case DeleteEvent:
var pl structs.DeletePayload
err = json.Unmarshal([]byte(payload), &pl)

if err == nil {
err = hook.verifySecret(pl.Secret)
}

return pl, err

case ForkEvent:
var pl structs.ForkPayload
err = json.Unmarshal([]byte(payload), &pl)

if err == nil {
err = hook.verifySecret(pl.Secret)
}

return pl, err

case IssuesEvent:
var pl structs.IssuePayload
err = json.Unmarshal([]byte(payload), &pl)

if err == nil {
err = hook.verifySecret(pl.Secret)
}

return pl, err

case IssueCommentEvent:
var pl structs.IssueCommentPayload
err = json.Unmarshal([]byte(payload), &pl)

if err == nil {
err = hook.verifySecret(pl.Secret)
}

return pl, err

case PullRequestEvent:
var pl structs.PullRequestPayload
err = json.Unmarshal([]byte(payload), &pl)

if err == nil {
err = hook.verifySecret(pl.Secret)
}

return pl, err

default:
return nil, fmt.Errorf("unknown event %s", giteaEvent)
}
}
174 changes: 174 additions & 0 deletions gitea/gitea_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
package gitea_test

import (
"bytes"
"io"
"net/http"
"net/http/httptest"
"os"
"reflect"
"testing"

"code.gitea.io/gitea/modules/structs"
"github.com/stretchr/testify/require"
"gopkg.in/go-playground/webhooks.v5/gitea"
)

const (
path = "/webhooks"
)

func newServer(handler http.HandlerFunc) *httptest.Server {
mux := http.NewServeMux()
mux.HandleFunc(path, handler)
return httptest.NewServer(mux)
}

func TestWebhooks(t *testing.T) {
assert := require.New(t)
tests := []struct {
name string
event gitea.Event
typ interface{}
filename string
headers http.Header
}{
{
name: "PushEvent",
event: gitea.PushEvent,
typ: structs.PushPayload{},
filename: "../testdata/gitea/push-event.json",
headers: http.Header{
"X-Gitea-Event": []string{"push"},
},
},
}

hook, _ := gitea.New(gitea.Options.Secret("mytoken"))

for _, tt := range tests {
tc := tt
client := &http.Client{}
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
payload, err := os.Open(tc.filename)
assert.NoError(err)
defer func() {
_ = payload.Close()
}()

var parseError error
var results interface{}
server := newServer(func(w http.ResponseWriter, r *http.Request) {
results, parseError = hook.Parse(r, tc.event)
})
defer server.Close()
req, err := http.NewRequest(http.MethodPost, server.URL+path, payload)
assert.NoError(err)
req.Header = tc.headers
req.Header.Set("Content-Type", "application/json")
req.Header.Set("X-Gitea-Token", "mytoken")

resp, err := client.Do(req)
assert.NoError(err)
assert.Equal(http.StatusOK, resp.StatusCode)
assert.NoError(parseError)
assert.Equal(reflect.TypeOf(tc.typ), reflect.TypeOf(results))
})
}
}

func TestBadRequests(t *testing.T) {
assert := require.New(t)
tests := []struct {
name string
event gitea.Event
payload io.Reader
headers http.Header
method string
expcectedError error
}{
{
name: "ErrEventNotSpecifiedToParse",
event: "",
method: http.MethodPost,
payload: bytes.NewBuffer([]byte("{}")),
headers: http.Header{},
expcectedError: gitea.ErrEventNotSpecifiedToParse,
},
{
name: "ErrInvalidHTTPMethod",
event: gitea.PushEvent,
method: http.MethodGet,
payload: bytes.NewBuffer([]byte("{}")),
headers: http.Header{},
expcectedError: gitea.ErrInvalidHTTPMethod,
},
{
name: "ErrMissingGiteaEventHeader",
event: gitea.PushEvent,
method: http.MethodPost,
payload: bytes.NewBuffer([]byte("{}")),
headers: http.Header{},
expcectedError: gitea.ErrMissingGiteaEventHeader,
},
{
name: "ErrEventNotFound",
event: gitea.PushEvent,
method: http.MethodPost,
payload: bytes.NewBuffer([]byte("{}")),
headers: http.Header{
"X-Gitea-Event": []string{"create"},
},
expcectedError: gitea.ErrEventNotFound,
},
{
name: "ErrParsingPayload",
event: gitea.PushEvent,
method: http.MethodPost,
payload: bytes.NewBuffer([]byte("")),
headers: http.Header{
"X-Gitea-Event": []string{"push"},
},
expcectedError: gitea.ErrParsingPayload,
},
{
name: "ErrSecretNotMatch",
event: gitea.PushEvent,
method: http.MethodPost,
payload: bytes.NewBuffer([]byte("{\"secret\":\"test\"}")),
headers: http.Header{
"X-Gitea-Event": []string{"push"},
},
expcectedError: gitea.ErrSecretNotMatch,
},
}

hook, _ := gitea.New(gitea.Options.Secret("mytoken"))

for _, tt := range tests {
tc := tt
client := &http.Client{}
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
var parseError error
server := newServer(func(w http.ResponseWriter, r *http.Request) {
if tc.event != "" {
_, parseError = hook.Parse(r, tc.event)
} else {
_, parseError = hook.Parse(r)
}
})
defer server.Close()
req, err := http.NewRequest(tc.method, server.URL+path, tc.payload)
assert.NoError(err)
req.Header = tc.headers
req.Header.Set("Content-Type", "application/json")

resp, err := client.Do(req)
assert.NoError(err)
assert.Equal(http.StatusOK, resp.StatusCode)
assert.Equal(tc.expcectedError, parseError)
})
}
}
13 changes: 13 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
module gopkg.in/go-playground/webhooks.v5

go 1.12

require (
code.gitea.io/git v0.0.0-20190411170847-63b74d438b29 // indirect
code.gitea.io/gitea v1.9.0-dev.0.20190511102134-34eee25bd42d
code.gitea.io/sdk/gitea v0.0.0-20190602153954-7e711e06b588 // indirect
github.com/go-gitea/gitea v1.8.2
github.com/gogits/go-gogs-client v0.0.0-20181217004319-1cd0db3113de
github.com/mvdan/xurls v1.1.0 // indirect
github.com/stretchr/testify v1.3.0
)
Loading