Skip to content

Commit

Permalink
Add Chinese code platform gitee webhooks
Browse files Browse the repository at this point in the history
  • Loading branch information
li-clement authored and Clement Li committed Apr 22, 2022
1 parent 9c954e2 commit 2755e3c
Show file tree
Hide file tree
Showing 10 changed files with 2,654 additions and 0 deletions.
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

0 comments on commit 2755e3c

Please sign in to comment.