Skip to content

Commit 2755e3c

Browse files
li-clementClement Li
authored and
Clement Li
committed
Add Chinese code platform gitee webhooks
1 parent 9c954e2 commit 2755e3c

10 files changed

+2654
-0
lines changed

gitee/gitee.go

+162
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
package gitee
2+
3+
import (
4+
"encoding/json"
5+
"errors"
6+
"fmt"
7+
"io"
8+
"io/ioutil"
9+
"net/http"
10+
)
11+
12+
// parse errors
13+
var (
14+
ErrMethodNotAllowed = errors.New("method not allowed")
15+
ErrMissingEvents = errors.New("missing X-Gitee-Events")
16+
ErrMissingEventHeader = errors.New("missing X-Gitee-Event Header")
17+
ErrMissingTimestampHeader = errors.New("missing X-Gitee-Timestamp Header")
18+
ErrMissingToken = errors.New("missing X-Gitee-Token")
19+
ErrContentType = errors.New("hook only accepts content-type: application/json")
20+
ErrRequestBody = errors.New("failed to read request body")
21+
ErrGiteeTokenVerificationFailed = errors.New("failed to verify token")
22+
ErrParsingPayload = errors.New("failed to parsing payload")
23+
ErrEventNotFound = errors.New("failed to find event")
24+
// ErrHMACVerificationFailed = errors.New("HMAC verification failed")
25+
)
26+
27+
// Gitee hook types
28+
const (
29+
PushEvents Event = "Push Hook"
30+
TagEvents Event = "Tag Push Hook"
31+
IssuesEvents Event = "Issue Hook"
32+
CommentEvents Event = "Note Hook"
33+
MergeRequestEvents Event = "Merge Request Hook"
34+
)
35+
36+
// Option is a configuration option for the webhook
37+
type Option func(*Webhook) error
38+
39+
// Options is a namespace var for configuration options
40+
var Options = WebhookOptions{}
41+
42+
// WebhookOptions is a namespace for configuration option methods
43+
type WebhookOptions struct{}
44+
45+
// Secret registers the Gitee secret
46+
func (WebhookOptions) Secret(secret string) Option {
47+
return func(hook *Webhook) error {
48+
hook.secret = secret
49+
return nil
50+
}
51+
}
52+
53+
// Webhook instance contains all methods needed to process events
54+
type Webhook struct {
55+
secret string
56+
}
57+
58+
// Event defines a Gitee hook event type by the X-Gitee-Event Header
59+
type Event string
60+
61+
// New creates and returns a WebHook instance denoted by the Provider type
62+
func New(options ...Option) (*Webhook, error) {
63+
hook := new(Webhook)
64+
for _, opt := range options {
65+
if err := opt(hook); err != nil {
66+
return nil, errors.New("error applying Option")
67+
}
68+
}
69+
return hook, nil
70+
}
71+
72+
// Parse verifies and parses the events specified and returns the payload object or an error
73+
func (hook Webhook) Parse(r *http.Request, events ...Event) (interface{}, error) {
74+
defer func() {
75+
_, _ = io.Copy(ioutil.Discard, r.Body)
76+
_ = r.Body.Close()
77+
}()
78+
79+
if len(events) == 0 {
80+
return nil, ErrMissingEvents
81+
}
82+
if r.Method != http.MethodPost {
83+
return nil, ErrMethodNotAllowed
84+
}
85+
86+
timeStamp := r.Header.Get("X-Gitee-Timestamp")
87+
if len(timeStamp) == 0 {
88+
return nil, ErrMissingTimestampHeader
89+
}
90+
91+
contentType := r.Header.Get("content-type")
92+
if contentType != "application/json" {
93+
return nil, ErrContentType
94+
}
95+
96+
event := r.Header.Get("X-Gitee-Event")
97+
if len(event) == 0 {
98+
return nil, ErrMissingEventHeader
99+
}
100+
101+
giteeEvent := Event(event)
102+
103+
payload, err := ioutil.ReadAll(r.Body)
104+
if err != nil || len(payload) == 0 {
105+
return nil, ErrParsingPayload
106+
}
107+
108+
// If we have a Secret set, we should check the MAC
109+
if len(hook.secret) > 0 {
110+
signature := r.Header.Get("X-Gitee-Token")
111+
if signature != hook.secret {
112+
return nil, ErrGiteeTokenVerificationFailed
113+
}
114+
}
115+
116+
return eventParsing(giteeEvent, events, payload)
117+
}
118+
119+
func eventParsing(giteeEvent Event, events []Event, payload []byte) (interface{}, error) {
120+
121+
var found bool
122+
for _, evt := range events {
123+
if evt == giteeEvent {
124+
found = true
125+
break
126+
}
127+
}
128+
// event not defined to be parsed
129+
if !found {
130+
return nil, ErrEventNotFound
131+
}
132+
133+
switch giteeEvent {
134+
case PushEvents:
135+
var pl PushEventPayload
136+
err := json.Unmarshal([]byte(payload), &pl)
137+
return pl, err
138+
139+
case TagEvents:
140+
var pl TagEventPayload
141+
err := json.Unmarshal([]byte(payload), &pl)
142+
return pl, err
143+
144+
case IssuesEvents:
145+
var pl IssueEventPayload
146+
err := json.Unmarshal([]byte(payload), &pl)
147+
return pl, err
148+
149+
case CommentEvents:
150+
var pl CommentEventPayload
151+
err := json.Unmarshal([]byte(payload), &pl)
152+
return pl, err
153+
154+
case MergeRequestEvents:
155+
var pl MergeRequestEventPayload
156+
err := json.Unmarshal([]byte(payload), &pl)
157+
return pl, err
158+
159+
default:
160+
return nil, fmt.Errorf("unknown event %s", giteeEvent)
161+
}
162+
}

gitee/gitee_test.go

+222
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,222 @@
1+
package gitee
2+
3+
import (
4+
"bytes"
5+
"io"
6+
"log"
7+
"net/http"
8+
"net/http/httptest"
9+
"os"
10+
"reflect"
11+
"testing"
12+
13+
"github.com/stretchr/testify/require"
14+
)
15+
16+
// NOTES:
17+
// - Run "go test" to run tests
18+
// - Run "gocov test | gocov report" to report on test converage by file
19+
// - Run "gocov test | gocov annotate -" to report on all code and functions, those ,marked with "MISS" were never called
20+
//
21+
// or
22+
//
23+
// -- may be a good idea to change to output path to somewherelike /tmp
24+
// go test -coverprofile cover.out && go tool cover -html=cover.out -o cover.html
25+
//
26+
27+
const (
28+
path = "/webhooks"
29+
)
30+
31+
var hook *Webhook
32+
33+
func TestMain(m *testing.M) {
34+
35+
// setup
36+
var err error
37+
hook, err = New(Options.Secret("sampleToken!"))
38+
if err != nil {
39+
log.Fatal(err)
40+
}
41+
os.Exit(m.Run())
42+
43+
// teardown
44+
}
45+
46+
func newServer(handler http.HandlerFunc) *httptest.Server {
47+
mux := http.NewServeMux()
48+
mux.HandleFunc(path, handler)
49+
return httptest.NewServer(mux)
50+
}
51+
52+
func TestBadRequests(t *testing.T) {
53+
assert := require.New(t)
54+
tests := []struct {
55+
name string
56+
event Event
57+
payload io.Reader
58+
headers http.Header
59+
}{
60+
{
61+
name: "BadNoEventHeader",
62+
event: PushEvents,
63+
payload: bytes.NewBuffer([]byte("{}")),
64+
headers: http.Header{},
65+
},
66+
{
67+
name: "UnsubscribedEvent",
68+
event: PushEvents,
69+
payload: bytes.NewBuffer([]byte("{}")),
70+
headers: http.Header{
71+
"X-Gitee-Event": []string{"noneexistant_event"},
72+
},
73+
},
74+
{
75+
name: "BadBody",
76+
event: PushEvents,
77+
payload: bytes.NewBuffer([]byte("")),
78+
headers: http.Header{
79+
"X-Gitee-Event": []string{"Push Hook"},
80+
"X-Gitee-Token": []string{"sampleToken!"},
81+
},
82+
},
83+
{
84+
name: "TokenMismatch",
85+
event: PushEvents,
86+
payload: bytes.NewBuffer([]byte("{}")),
87+
headers: http.Header{
88+
"X-Gitee-Event": []string{"Push Hook"},
89+
"X-Gitee-Token": []string{"badsampleToken!!"},
90+
},
91+
},
92+
}
93+
94+
for _, tt := range tests {
95+
tc := tt
96+
client := &http.Client{}
97+
t.Run(tt.name, func(t *testing.T) {
98+
t.Parallel()
99+
var parseError error
100+
server := newServer(func(w http.ResponseWriter, r *http.Request) {
101+
_, parseError = hook.Parse(r, tc.event)
102+
})
103+
defer server.Close()
104+
req, err := http.NewRequest(http.MethodPost, server.URL+path, tc.payload)
105+
assert.NoError(err)
106+
req.Header = tc.headers
107+
req.Header.Set("Content-Type", "application/json")
108+
109+
resp, err := client.Do(req)
110+
assert.NoError(err)
111+
assert.Equal(http.StatusOK, resp.StatusCode)
112+
assert.Error(parseError)
113+
})
114+
}
115+
}
116+
117+
func TestWebhooks(t *testing.T) {
118+
assert := require.New(t)
119+
tests := []struct {
120+
name string
121+
event Event
122+
typ interface{}
123+
filename string
124+
headers http.Header
125+
}{
126+
{
127+
name: "PushEvent",
128+
event: PushEvents,
129+
typ: PushEventPayload{},
130+
filename: "../testdata/gitee/push-event.json",
131+
headers: http.Header{
132+
"X-Gitee-Event": []string{"Push Hook"},
133+
},
134+
},
135+
{
136+
name: "TagEvent",
137+
event: TagEvents,
138+
typ: TagEventPayload{},
139+
filename: "../testdata/gitee/tag-event.json",
140+
headers: http.Header{
141+
"X-Gitee-Event": []string{"Tag Push Hook"},
142+
},
143+
},
144+
{
145+
name: "IssueEvent",
146+
event: IssuesEvents,
147+
typ: IssueEventPayload{},
148+
filename: "../testdata/gitee/issue-event.json",
149+
headers: http.Header{
150+
"X-Gitee-Event": []string{"Issue Hook"},
151+
},
152+
},
153+
{
154+
name: "CommentCommitEvent",
155+
event: CommentEvents,
156+
typ: CommentEventPayload{},
157+
filename: "../testdata/gitee/comment-commit-event.json",
158+
headers: http.Header{
159+
"X-Gitee-Event": []string{"Note Hook"},
160+
},
161+
},
162+
{
163+
name: "CommentMergeRequestEvent",
164+
event: CommentEvents,
165+
typ: CommentEventPayload{},
166+
filename: "../testdata/gitee/comment-merge-request-event.json",
167+
headers: http.Header{
168+
"X-Gitee-Event": []string{"Note Hook"},
169+
},
170+
},
171+
{
172+
name: "CommentIssueEvent",
173+
event: CommentEvents,
174+
typ: CommentEventPayload{},
175+
filename: "../testdata/gitee/comment-issue-event.json",
176+
headers: http.Header{
177+
"X-Gitee-Event": []string{"Note Hook"},
178+
},
179+
},
180+
{
181+
name: "MergeRequestEvent",
182+
event: MergeRequestEvents,
183+
typ: MergeRequestEventPayload{},
184+
filename: "../testdata/gitee/merge-request-event.json",
185+
headers: http.Header{
186+
"X-Gitee-Event": []string{"Merge Request Hook"},
187+
},
188+
},
189+
}
190+
191+
for _, tt := range tests {
192+
tc := tt
193+
client := &http.Client{}
194+
t.Run(tt.name, func(t *testing.T) {
195+
t.Parallel()
196+
payload, err := os.Open(tc.filename)
197+
assert.NoError(err)
198+
defer func() {
199+
_ = payload.Close()
200+
}()
201+
202+
var parseError error
203+
var results interface{}
204+
server := newServer(func(w http.ResponseWriter, r *http.Request) {
205+
results, parseError = hook.Parse(r, tc.event)
206+
})
207+
defer server.Close()
208+
req, err := http.NewRequest(http.MethodPost, server.URL+path, payload)
209+
assert.NoError(err)
210+
req.Header = tc.headers
211+
req.Header.Set("Content-Type", "application/json")
212+
req.Header.Set("X-Gitee-Token", "sampleToken!")
213+
req.Header.Set("X-Gitee-TimeStamp", "1650090527447")
214+
215+
resp, err := client.Do(req)
216+
assert.NoError(err)
217+
assert.Equal(http.StatusOK, resp.StatusCode)
218+
assert.NoError(parseError)
219+
assert.Equal(reflect.TypeOf(tc.typ), reflect.TypeOf(results))
220+
})
221+
}
222+
}

0 commit comments

Comments
 (0)