1
2
3
4
5
6 package github
7
8 import (
9 "bytes"
10 "encoding/json"
11 "errors"
12 "fmt"
13 "net/http"
14 "net/url"
15 "strings"
16 "testing"
17
18 "github.com/google/go-cmp/cmp"
19 )
20
21 func TestMessageMAC_BadHashTypePrefix(t *testing.T) {
22 const signature = "bogus1=1234567"
23 if _, _, err := messageMAC(signature); err == nil {
24 t.Fatal("messageMAC returned nil; wanted error")
25 }
26 }
27
28 func TestValidatePayload(t *testing.T) {
29 const defaultBody = `{"yo":true}`
30 const defaultSignature = "sha1=126f2c800419c60137ce748d7672e77b65cf16d6"
31 secretKey := []byte("0123456789abcdef")
32 tests := []struct {
33 signature string
34 signatureHeader string
35 eventID string
36 event string
37 wantEventID string
38 wantEvent string
39 wantPayload string
40 }{
41
42 {},
43 {signature: "yo"},
44 {signature: "sha1=yo"},
45 {signature: "sha1=012345"},
46
47 {
48 signature: defaultSignature,
49 eventID: "dead-beef",
50 event: "ping",
51 wantEventID: "dead-beef",
52 wantEvent: "ping",
53 wantPayload: defaultBody,
54 },
55 {
56 signature: defaultSignature,
57 event: "ping",
58 wantEvent: "ping",
59 wantPayload: defaultBody,
60 },
61 {
62 signature: "sha256=b1f8020f5b4cd42042f807dd939015c4a418bc1ff7f604dd55b0a19b5d953d9b",
63 event: "ping",
64 wantEvent: "ping",
65 wantPayload: defaultBody,
66 },
67 {
68 signature: "sha256=b1f8020f5b4cd42042f807dd939015c4a418bc1ff7f604dd55b0a19b5d953d9b",
69 signatureHeader: SHA256SignatureHeader,
70 event: "ping",
71 wantEvent: "ping",
72 wantPayload: defaultBody,
73 },
74 {
75 signature: "sha512=8456767023c1195682e182a23b3f5d19150ecea598fde8cb85918f7281b16079471b1329f92b912c4d8bd7455cb159777db8f29608b20c7c87323ba65ae62e1f",
76 event: "ping",
77 wantEvent: "ping",
78 wantPayload: defaultBody,
79 },
80 }
81
82 for _, test := range tests {
83 buf := bytes.NewBufferString(defaultBody)
84 req, err := http.NewRequest("GET", "http://localhost/event", buf)
85 if err != nil {
86 t.Fatalf("NewRequest: %v", err)
87 }
88 if test.signature != "" {
89 if test.signatureHeader != "" {
90 req.Header.Set(test.signatureHeader, test.signature)
91 } else {
92 req.Header.Set(SHA1SignatureHeader, test.signature)
93 }
94 }
95 req.Header.Set("Content-Type", "application/json")
96
97 got, err := ValidatePayload(req, secretKey)
98 if err != nil {
99 if test.wantPayload != "" {
100 t.Errorf("ValidatePayload(%#v): err = %v, want nil", test, err)
101 }
102 continue
103 }
104 if string(got) != test.wantPayload {
105 t.Errorf("ValidatePayload = %q, want %q", got, test.wantPayload)
106 }
107 }
108 }
109
110 func TestValidatePayload_FormGet(t *testing.T) {
111 payload := `{"yo":true}`
112 signature := "sha1=3374ef144403e8035423b23b02e2c9d7a4c50368"
113 secretKey := []byte("0123456789abcdef")
114
115 form := url.Values{}
116 form.Add("payload", payload)
117 req, err := http.NewRequest("POST", "http://localhost/event", strings.NewReader(form.Encode()))
118 if err != nil {
119 t.Fatalf("NewRequest: %v", err)
120 }
121 req.PostForm = form
122 req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
123 req.Header.Set(SHA1SignatureHeader, signature)
124
125 got, err := ValidatePayload(req, secretKey)
126 if err != nil {
127 t.Errorf("ValidatePayload(%#v): err = %v, want nil", payload, err)
128 }
129 if string(got) != payload {
130 t.Errorf("ValidatePayload = %q, want %q", got, payload)
131 }
132
133
134 req.Header.Set(SHA1SignatureHeader, "invalid signature")
135 if _, err = ValidatePayload(req, []byte{0}); err == nil {
136 t.Error("ValidatePayload = nil, want err")
137 }
138 }
139
140 func TestValidatePayload_FormPost(t *testing.T) {
141 payload := `{"yo":true}`
142 signature := "sha1=3374ef144403e8035423b23b02e2c9d7a4c50368"
143 secretKey := []byte("0123456789abcdef")
144
145 form := url.Values{}
146 form.Set("payload", payload)
147 buf := bytes.NewBufferString(form.Encode())
148 req, err := http.NewRequest("POST", "http://localhost/event", buf)
149 if err != nil {
150 t.Fatalf("NewRequest: %v", err)
151 }
152 req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
153 req.Header.Set(SHA1SignatureHeader, signature)
154
155 got, err := ValidatePayload(req, secretKey)
156 if err != nil {
157 t.Errorf("ValidatePayload(%#v): err = %v, want nil", payload, err)
158 }
159 if string(got) != payload {
160 t.Errorf("ValidatePayload = %q, want %q", got, payload)
161 }
162
163
164 req.Header.Set(SHA1SignatureHeader, "invalid signature")
165 if _, err = ValidatePayload(req, []byte{0}); err == nil {
166 t.Error("ValidatePayload = nil, want err")
167 }
168 }
169
170 func TestValidatePayload_InvalidContentType(t *testing.T) {
171 req, err := http.NewRequest("POST", "http://localhost/event", nil)
172 if err != nil {
173 t.Fatalf("NewRequest: %v", err)
174 }
175 req.Header.Set("Content-Type", "invalid content type")
176 if _, err = ValidatePayload(req, nil); err == nil {
177 t.Error("ValidatePayload = nil, want err")
178 }
179 }
180
181 func TestValidatePayload_NoSecretKey(t *testing.T) {
182 payload := `{"yo":true}`
183
184 form := url.Values{}
185 form.Set("payload", payload)
186 buf := bytes.NewBufferString(form.Encode())
187 req, err := http.NewRequest("POST", "http://localhost/event", buf)
188 if err != nil {
189 t.Fatalf("NewRequest: %v", err)
190 }
191 req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
192
193 got, err := ValidatePayload(req, nil)
194 if err != nil {
195 t.Errorf("ValidatePayload(%#v): err = %v, want nil", payload, err)
196 }
197 if string(got) != payload {
198 t.Errorf("ValidatePayload = %q, want %q", got, payload)
199 }
200 }
201
202
203 type badReader struct{}
204
205 func (b *badReader) Read(p []byte) (int, error) {
206 return 0, errors.New("bad reader")
207 }
208
209 func (b *badReader) Close() error { return errors.New("bad reader") }
210
211 func TestValidatePayload_BadRequestBody(t *testing.T) {
212 tests := []struct {
213 contentType string
214 }{
215 {contentType: "application/json"},
216 {contentType: "application/x-www-form-urlencoded"},
217 }
218
219 for i, tt := range tests {
220 t.Run(fmt.Sprintf("test #%v", i), func(t *testing.T) {
221 req := &http.Request{
222 Header: http.Header{"Content-Type": []string{tt.contentType}},
223 Body: &badReader{},
224 }
225 if _, err := ValidatePayload(req, nil); err == nil {
226 t.Fatal("ValidatePayload returned nil; want error")
227 }
228 })
229 }
230 }
231
232 func TestValidatePayload_InvalidContentTypeParams(t *testing.T) {
233 req, err := http.NewRequest("POST", "http://localhost/event", nil)
234 if err != nil {
235 t.Fatalf("NewRequest: %v", err)
236 }
237 req.Header.Set("Content-Type", "application/json; charset=")
238 if _, err = ValidatePayload(req, nil); err == nil {
239 t.Error("ValidatePayload = nil, want err")
240 }
241 }
242
243 func TestValidatePayload_ValidContentTypeParams(t *testing.T) {
244 var requestBody = `{"yo":true}`
245 buf := bytes.NewBufferString(requestBody)
246
247 req, err := http.NewRequest("POST", "http://localhost/event", buf)
248 if err != nil {
249 t.Fatalf("NewRequest: %v", err)
250 }
251 req.Header.Set("Content-Type", "application/json; charset=UTF-8")
252
253 _, err = ValidatePayload(req, nil)
254 if err != nil {
255 t.Error("ValidatePayload = nil, want err")
256 }
257 }
258
259 func TestParseWebHook(t *testing.T) {
260 tests := []struct {
261 payload interface{}
262 messageType string
263 }{
264 {
265 payload: &BranchProtectionRuleEvent{},
266 messageType: "branch_protection_rule",
267 },
268 {
269 payload: &CheckRunEvent{},
270 messageType: "check_run",
271 },
272 {
273 payload: &CheckSuiteEvent{},
274 messageType: "check_suite",
275 },
276 {
277 payload: &CommitCommentEvent{},
278 messageType: "commit_comment",
279 },
280 {
281 payload: &ContentReferenceEvent{},
282 messageType: "content_reference",
283 },
284 {
285 payload: &CreateEvent{},
286 messageType: "create",
287 },
288 {
289 payload: &DeleteEvent{},
290 messageType: "delete",
291 },
292 {
293 payload: &DeployKeyEvent{},
294 messageType: "deploy_key",
295 },
296 {
297 payload: &DeploymentEvent{},
298 messageType: "deployment",
299 },
300 {
301 payload: &DeploymentStatusEvent{},
302 messageType: "deployment_status",
303 },
304 {
305 payload: &DiscussionEvent{},
306 messageType: "discussion",
307 },
308 {
309 payload: &ForkEvent{},
310 messageType: "fork",
311 },
312 {
313 payload: &GitHubAppAuthorizationEvent{},
314 messageType: "github_app_authorization",
315 },
316 {
317 payload: &GollumEvent{},
318 messageType: "gollum",
319 },
320 {
321 payload: &InstallationEvent{},
322 messageType: "installation",
323 },
324 {
325 payload: &InstallationRepositoriesEvent{},
326 messageType: "installation_repositories",
327 },
328 {
329 payload: &IssueCommentEvent{},
330 messageType: "issue_comment",
331 },
332 {
333 payload: &IssuesEvent{},
334 messageType: "issues",
335 },
336 {
337 payload: &LabelEvent{},
338 messageType: "label",
339 },
340 {
341 payload: &MarketplacePurchaseEvent{},
342 messageType: "marketplace_purchase",
343 },
344 {
345 payload: &MemberEvent{},
346 messageType: "member",
347 },
348 {
349 payload: &MembershipEvent{},
350 messageType: "membership",
351 },
352 {
353 payload: &MetaEvent{},
354 messageType: "meta",
355 },
356 {
357 payload: &MilestoneEvent{},
358 messageType: "milestone",
359 },
360 {
361 payload: &OrganizationEvent{},
362 messageType: "organization",
363 },
364 {
365 payload: &OrgBlockEvent{},
366 messageType: "org_block",
367 },
368 {
369 payload: &PackageEvent{},
370 messageType: "package",
371 },
372 {
373 payload: &PageBuildEvent{},
374 messageType: "page_build",
375 },
376 {
377 payload: &PingEvent{},
378 messageType: "ping",
379 },
380 {
381 payload: &ProjectEvent{},
382 messageType: "project",
383 },
384 {
385 payload: &ProjectCardEvent{},
386 messageType: "project_card",
387 },
388 {
389 payload: &ProjectColumnEvent{},
390 messageType: "project_column",
391 },
392 {
393 payload: &PublicEvent{},
394 messageType: "public",
395 },
396 {
397 payload: &PullRequestEvent{},
398 messageType: "pull_request",
399 },
400 {
401 payload: &PullRequestReviewEvent{},
402 messageType: "pull_request_review",
403 },
404 {
405 payload: &PullRequestReviewCommentEvent{},
406 messageType: "pull_request_review_comment",
407 },
408 {
409 payload: &PullRequestReviewThreadEvent{},
410 messageType: "pull_request_review_thread",
411 },
412 {
413 payload: &PullRequestTargetEvent{},
414 messageType: "pull_request_target",
415 },
416 {
417 payload: &PushEvent{},
418 messageType: "push",
419 },
420 {
421 payload: &ReleaseEvent{},
422 messageType: "release",
423 },
424 {
425 payload: &RepositoryEvent{},
426 messageType: "repository",
427 },
428 {
429 payload: &RepositoryVulnerabilityAlertEvent{},
430 messageType: "repository_vulnerability_alert",
431 },
432 {
433 payload: &SecretScanningAlertEvent{},
434 messageType: "secret_scanning_alert",
435 },
436 {
437 payload: &StarEvent{},
438 messageType: "star",
439 },
440 {
441 payload: &StatusEvent{},
442 messageType: "status",
443 },
444 {
445 payload: &TeamEvent{},
446 messageType: "team",
447 },
448 {
449 payload: &TeamAddEvent{},
450 messageType: "team_add",
451 },
452 {
453 payload: &UserEvent{},
454 messageType: "user",
455 },
456 {
457 payload: &WatchEvent{},
458 messageType: "watch",
459 },
460 {
461 payload: &RepositoryImportEvent{},
462 messageType: "repository_import",
463 },
464 {
465 payload: &RepositoryDispatchEvent{},
466 messageType: "repository_dispatch",
467 },
468 {
469 payload: &WorkflowDispatchEvent{},
470 messageType: "workflow_dispatch",
471 },
472 {
473 payload: &WorkflowJobEvent{},
474 messageType: "workflow_job",
475 },
476 {
477 payload: &WorkflowRunEvent{},
478 messageType: "workflow_run",
479 },
480 }
481
482 for _, test := range tests {
483 p, err := json.Marshal(test.payload)
484 if err != nil {
485 t.Fatalf("Marshal(%#v): %v", test.payload, err)
486 }
487 got, err := ParseWebHook(test.messageType, p)
488 if err != nil {
489 t.Fatalf("ParseWebHook: %v", err)
490 }
491 if want := test.payload; !cmp.Equal(got, want) {
492 t.Errorf("ParseWebHook(%#v, %#v) = %#v, want %#v", test.messageType, p, got, want)
493 }
494 }
495 }
496
497 func TestParseWebHook_BadMessageType(t *testing.T) {
498 if _, err := ParseWebHook("bogus message type", []byte("{}")); err == nil {
499 t.Fatal("ParseWebHook returned nil; wanted error")
500 }
501 }
502
503 func TestValidatePayloadFromBody_UnableToParseBody(t *testing.T) {
504 if _, err := ValidatePayloadFromBody("application/x-www-form-urlencoded", bytes.NewReader([]byte(`%`)), "sha1=", []byte{}); err == nil {
505 t.Errorf("ValidatePayloadFromBody returned nil; wanted error")
506 }
507 }
508
509 func TestValidatePayloadFromBody_UnsupportedContentType(t *testing.T) {
510 if _, err := ValidatePayloadFromBody("invalid", bytes.NewReader([]byte(`{}`)), "sha1=", []byte{}); err == nil {
511 t.Errorf("ValidatePayloadFromBody returned nil; wanted error")
512 }
513 }
514
515 func TestDeliveryID(t *testing.T) {
516 id := "8970a780-244e-11e7-91ca-da3aabcb9793"
517 req, err := http.NewRequest("POST", "http://localhost", nil)
518 if err != nil {
519 t.Fatalf("DeliveryID: %v", err)
520 }
521 req.Header.Set("X-Github-Delivery", id)
522
523 got := DeliveryID(req)
524 if got != id {
525 t.Errorf("DeliveryID(%#v) = %q, want %q", req, got, id)
526 }
527 }
528
529 func TestWebHookType(t *testing.T) {
530 want := "yo"
531 req := &http.Request{
532 Header: http.Header{EventTypeHeader: []string{want}},
533 }
534 if got := WebHookType(req); got != want {
535 t.Errorf("WebHookType = %q, want %q", got, want)
536 }
537 }
538
View as plain text