-
Notifications
You must be signed in to change notification settings - Fork 237
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add Chinese code platform gitee webhooks
- Loading branch information
1 parent
9c954e2
commit 2755e3c
Showing
10 changed files
with
2,654 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)) | ||
}) | ||
} | ||
} |
Oops, something went wrong.