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 Chinese code platform gitee webhooks #147

Open
wants to merge 1 commit into
base: master
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
162 changes: 162 additions & 0 deletions gitee/gitee.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
package gitee

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

// parse errors
var (
ErrMethodNotAllowed = errors.New("method not allowed")
ErrMissingEvents = errors.New("missing X-Gitee-Events")
ErrMissingEventHeader = errors.New("missing X-Gitee-Event Header")
ErrMissingTimestampHeader = errors.New("missing X-Gitee-Timestamp Header")
ErrMissingToken = errors.New("missing X-Gitee-Token")
ErrContentType = errors.New("hook only accepts content-type: application/json")
ErrRequestBody = errors.New("failed to read request body")
ErrGiteeTokenVerificationFailed = errors.New("failed to verify token")
ErrParsingPayload = errors.New("failed to parsing payload")
ErrEventNotFound = errors.New("failed to find event")
// ErrHMACVerificationFailed = errors.New("HMAC verification failed")
)

// Gitee hook types
const (
PushEvents Event = "Push Hook"
TagEvents Event = "Tag Push Hook"
IssuesEvents Event = "Issue Hook"
CommentEvents Event = "Note Hook"
MergeRequestEvents Event = "Merge Request Hook"
)

// 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 Gitee 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 Gitee hook event type by the X-Gitee-Event Header
type Event string

// 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
}

// 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, ErrMissingEvents
}
if r.Method != http.MethodPost {
return nil, ErrMethodNotAllowed
}

timeStamp := r.Header.Get("X-Gitee-Timestamp")
if len(timeStamp) == 0 {
return nil, ErrMissingTimestampHeader
}

contentType := r.Header.Get("content-type")
if contentType != "application/json" {
return nil, ErrContentType
}

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

giteeEvent := Event(event)

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

// If we have a Secret set, we should check the MAC
if len(hook.secret) > 0 {
signature := r.Header.Get("X-Gitee-Token")
if signature != hook.secret {
return nil, ErrGiteeTokenVerificationFailed
}
}

return eventParsing(giteeEvent, events, payload)
}

func eventParsing(giteeEvent Event, events []Event, payload []byte) (interface{}, error) {

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

switch giteeEvent {
case PushEvents:
var pl PushEventPayload
err := json.Unmarshal([]byte(payload), &pl)
return pl, err

case TagEvents:
var pl TagEventPayload
err := json.Unmarshal([]byte(payload), &pl)
return pl, err

case IssuesEvents:
var pl IssueEventPayload
err := json.Unmarshal([]byte(payload), &pl)
return pl, err

case CommentEvents:
var pl CommentEventPayload
err := json.Unmarshal([]byte(payload), &pl)
return pl, err

case MergeRequestEvents:
var pl MergeRequestEventPayload
err := json.Unmarshal([]byte(payload), &pl)
return pl, err

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

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

"github.com/stretchr/testify/require"
)

// NOTES:
// - Run "go test" to run tests
// - Run "gocov test | gocov report" to report on test converage by file
// - Run "gocov test | gocov annotate -" to report on all code and functions, those ,marked with "MISS" were never called
//
// or
//
// -- may be a good idea to change to output path to somewherelike /tmp
// go test -coverprofile cover.out && go tool cover -html=cover.out -o cover.html
//

const (
path = "/webhooks"
)

var hook *Webhook

func TestMain(m *testing.M) {

// setup
var err error
hook, err = New(Options.Secret("sampleToken!"))
if err != nil {
log.Fatal(err)
}
os.Exit(m.Run())

// teardown
}

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

func TestBadRequests(t *testing.T) {
assert := require.New(t)
tests := []struct {
name string
event Event
payload io.Reader
headers http.Header
}{
{
name: "BadNoEventHeader",
event: PushEvents,
payload: bytes.NewBuffer([]byte("{}")),
headers: http.Header{},
},
{
name: "UnsubscribedEvent",
event: PushEvents,
payload: bytes.NewBuffer([]byte("{}")),
headers: http.Header{
"X-Gitee-Event": []string{"noneexistant_event"},
},
},
{
name: "BadBody",
event: PushEvents,
payload: bytes.NewBuffer([]byte("")),
headers: http.Header{
"X-Gitee-Event": []string{"Push Hook"},
"X-Gitee-Token": []string{"sampleToken!"},
},
},
{
name: "TokenMismatch",
event: PushEvents,
payload: bytes.NewBuffer([]byte("{}")),
headers: http.Header{
"X-Gitee-Event": []string{"Push Hook"},
"X-Gitee-Token": []string{"badsampleToken!!"},
},
},
}

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) {
_, parseError = hook.Parse(r, tc.event)
})
defer server.Close()
req, err := http.NewRequest(http.MethodPost, 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.Error(parseError)
})
}
}

func TestWebhooks(t *testing.T) {
assert := require.New(t)
tests := []struct {
name string
event Event
typ interface{}
filename string
headers http.Header
}{
{
name: "PushEvent",
event: PushEvents,
typ: PushEventPayload{},
filename: "../testdata/gitee/push-event.json",
headers: http.Header{
"X-Gitee-Event": []string{"Push Hook"},
},
},
{
name: "TagEvent",
event: TagEvents,
typ: TagEventPayload{},
filename: "../testdata/gitee/tag-event.json",
headers: http.Header{
"X-Gitee-Event": []string{"Tag Push Hook"},
},
},
{
name: "IssueEvent",
event: IssuesEvents,
typ: IssueEventPayload{},
filename: "../testdata/gitee/issue-event.json",
headers: http.Header{
"X-Gitee-Event": []string{"Issue Hook"},
},
},
{
name: "CommentCommitEvent",
event: CommentEvents,
typ: CommentEventPayload{},
filename: "../testdata/gitee/comment-commit-event.json",
headers: http.Header{
"X-Gitee-Event": []string{"Note Hook"},
},
},
{
name: "CommentMergeRequestEvent",
event: CommentEvents,
typ: CommentEventPayload{},
filename: "../testdata/gitee/comment-merge-request-event.json",
headers: http.Header{
"X-Gitee-Event": []string{"Note Hook"},
},
},
{
name: "CommentIssueEvent",
event: CommentEvents,
typ: CommentEventPayload{},
filename: "../testdata/gitee/comment-issue-event.json",
headers: http.Header{
"X-Gitee-Event": []string{"Note Hook"},
},
},
{
name: "MergeRequestEvent",
event: MergeRequestEvents,
typ: MergeRequestEventPayload{},
filename: "../testdata/gitee/merge-request-event.json",
headers: http.Header{
"X-Gitee-Event": []string{"Merge Request Hook"},
},
},
}

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-Gitee-Token", "sampleToken!")
req.Header.Set("X-Gitee-TimeStamp", "1650090527447")

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))
})
}
}
Loading