github.com/zppinho/prow@v0.0.0-20240510014325-1738badeb017/pkg/hook/server_test.go (about) 1 /* 2 Copyright 2016 The Kubernetes Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package hook 18 19 import ( 20 "bytes" 21 "io" 22 "net/http" 23 "net/http/httptest" 24 "reflect" 25 "strings" 26 "sync" 27 "testing" 28 29 "github.com/google/go-cmp/cmp" 30 "github.com/google/go-cmp/cmp/cmpopts" 31 32 "sigs.k8s.io/prow/pkg/githubeventserver" 33 "sigs.k8s.io/prow/pkg/plugins" 34 ) 35 36 func TestServeHTTPErrors(t *testing.T) { 37 metrics := githubeventserver.NewMetrics() 38 pa := &plugins.ConfigAgent{} 39 pa.Set(&plugins.Configuration{}) 40 41 getSecret := func() []byte { 42 var repoLevelSecret = ` 43 '*': 44 - value: abc 45 created_at: 2019-10-02T15:00:00Z 46 - value: key2 47 created_at: 2020-10-02T15:00:00Z 48 foo/bar: 49 - value: 123abc 50 created_at: 2019-10-02T15:00:00Z 51 - value: key6 52 created_at: 2020-10-02T15:00:00Z 53 ` 54 return []byte(repoLevelSecret) 55 } 56 57 s := &Server{ 58 Metrics: metrics, 59 Plugins: pa, 60 TokenGenerator: getSecret, 61 RepoEnabled: func(org, repo string) bool { return true }, 62 } 63 // This is the SHA1 signature for payload "{}" and signature "abc" 64 // echo -n '{}' | openssl dgst -sha1 -hmac abc 65 const hmac string = "sha1=db5c76f4264d0ad96cf21baec394964b4b8ce580" 66 const body string = "{}" 67 var testcases = []struct { 68 name string 69 70 Method string 71 Header map[string]string 72 Body string 73 Code int 74 }{ 75 { 76 name: "Delete", 77 78 Method: http.MethodDelete, 79 Header: map[string]string{ 80 "X-GitHub-Event": "ping", 81 "X-GitHub-Delivery": "I am unique", 82 "X-Hub-Signature": hmac, 83 "content-type": "application/json", 84 }, 85 Body: body, 86 Code: http.StatusMethodNotAllowed, 87 }, 88 { 89 name: "No event", 90 91 Method: http.MethodPost, 92 Header: map[string]string{ 93 "X-GitHub-Delivery": "I am unique", 94 "X-Hub-Signature": hmac, 95 "content-type": "application/json", 96 }, 97 Body: body, 98 Code: http.StatusBadRequest, 99 }, 100 { 101 name: "No content type", 102 103 Method: http.MethodPost, 104 Header: map[string]string{ 105 "X-GitHub-Event": "ping", 106 "X-GitHub-Delivery": "I am unique", 107 "X-Hub-Signature": hmac, 108 }, 109 Body: body, 110 Code: http.StatusBadRequest, 111 }, 112 { 113 name: "No event guid", 114 115 Method: http.MethodPost, 116 Header: map[string]string{ 117 "X-GitHub-Event": "ping", 118 "X-Hub-Signature": hmac, 119 "content-type": "application/json", 120 }, 121 Body: body, 122 Code: http.StatusBadRequest, 123 }, 124 { 125 name: "No signature", 126 127 Method: http.MethodPost, 128 Header: map[string]string{ 129 "X-GitHub-Event": "ping", 130 "X-GitHub-Delivery": "I am unique", 131 "content-type": "application/json", 132 }, 133 Body: body, 134 Code: http.StatusForbidden, 135 }, 136 { 137 name: "Bad signature", 138 139 Method: http.MethodPost, 140 Header: map[string]string{ 141 "X-GitHub-Event": "ping", 142 "X-GitHub-Delivery": "I am unique", 143 "X-Hub-Signature": "this doesn't work", 144 "content-type": "application/json", 145 }, 146 Body: body, 147 Code: http.StatusForbidden, 148 }, 149 { 150 name: "Good", 151 152 Method: http.MethodPost, 153 Header: map[string]string{ 154 "X-GitHub-Event": "ping", 155 "X-GitHub-Delivery": "I am unique", 156 "X-Hub-Signature": hmac, 157 "content-type": "application/json", 158 }, 159 Body: body, 160 Code: http.StatusOK, 161 }, 162 { 163 name: "Good, again", 164 165 Method: http.MethodGet, 166 Header: map[string]string{ 167 "content-type": "application/json", 168 }, 169 Body: body, 170 Code: http.StatusMethodNotAllowed, 171 }, 172 } 173 174 for _, tc := range testcases { 175 t.Logf("Running scenario %q", tc.name) 176 177 w := httptest.NewRecorder() 178 r, err := http.NewRequest(tc.Method, "", strings.NewReader(tc.Body)) 179 if err != nil { 180 t.Fatal(err) 181 } 182 for k, v := range tc.Header { 183 r.Header.Set(k, v) 184 } 185 s.ServeHTTP(w, r) 186 if w.Code != tc.Code { 187 t.Errorf("For test case: %+v\nExpected code %v, got code %v", tc, tc.Code, w.Code) 188 } 189 } 190 } 191 192 func TestNeedDemux(t *testing.T) { 193 tests := []struct { 194 name string 195 196 eventType string 197 srcRepo string 198 repoEnabled func(org, repo string) bool 199 plugins map[string][]plugins.ExternalPlugin 200 201 expected []plugins.ExternalPlugin 202 }{ 203 { 204 name: "no external plugins", 205 206 eventType: "issue_comment", 207 srcRepo: "kubernetes/test-infra", 208 plugins: nil, 209 210 expected: nil, 211 }, 212 { 213 name: "we have variety", 214 215 eventType: "issue_comment", 216 srcRepo: "kubernetes/test-infra", 217 plugins: map[string][]plugins.ExternalPlugin{ 218 "kubernetes/test-infra": { 219 { 220 Name: "sandwich", 221 Events: []string{"pull_request"}, 222 }, 223 { 224 Name: "coffee", 225 }, 226 }, 227 "kubernetes/kubernetes": { 228 { 229 Name: "gumbo", 230 Events: []string{"issue_comment"}, 231 }, 232 }, 233 "kubernetes": { 234 { 235 Name: "chicken", 236 Events: []string{"push"}, 237 }, 238 { 239 Name: "water", 240 }, 241 { 242 Name: "chocolate", 243 Events: []string{"pull_request", "issue_comment", "issues"}, 244 }, 245 }, 246 }, 247 248 expected: []plugins.ExternalPlugin{ 249 { 250 Name: "coffee", 251 }, 252 { 253 Name: "water", 254 }, 255 { 256 Name: "chocolate", 257 Events: []string{"pull_request", "issue_comment", "issues"}, 258 }, 259 }, 260 }, 261 { 262 name: "external plugins handling other events", 263 264 eventType: "repository", 265 srcRepo: "kubernetes/test-infra", 266 plugins: map[string][]plugins.ExternalPlugin{ 267 "kubernetes/test-infra": { 268 { 269 Name: "coffee", 270 }, 271 }, 272 "kubernetes/kubernetes": { 273 { 274 Name: "gumbo", 275 Events: []string{"issue_comment"}, 276 }, 277 }, 278 "kubernetes": { 279 { 280 Name: "chicken", 281 Events: []string{"repository"}, 282 }, 283 { 284 Name: "water", 285 }, 286 { 287 Name: "chocolate", 288 Events: []string{"pull_request", "issue_comment", "repository"}, 289 }, 290 }, 291 }, 292 293 expected: []plugins.ExternalPlugin{ 294 { 295 Name: "coffee", 296 }, 297 { 298 Name: "water", 299 }, 300 { 301 Name: "chicken", 302 Events: []string{"repository"}, 303 }, 304 { 305 Name: "chocolate", 306 Events: []string{"pull_request", "issue_comment", "repository"}, 307 }, 308 }, 309 }, 310 { 311 name: "we have variety but disabled that repo", 312 313 eventType: "issue_comment", 314 srcRepo: "kubernetes/test-infra", 315 repoEnabled: func(org, repo string) bool { 316 if org == "kubernetes" && repo == "test-infra" { 317 return false 318 } 319 return true 320 }, 321 plugins: map[string][]plugins.ExternalPlugin{ 322 "kubernetes/test-infra": { 323 { 324 Name: "sandwich", 325 Events: []string{"pull_request"}, 326 }, 327 { 328 Name: "coffee", 329 }, 330 }, 331 "kubernetes/kubernetes": { 332 { 333 Name: "gumbo", 334 Events: []string{"issue_comment"}, 335 }, 336 }, 337 "kubernetes": { 338 { 339 Name: "chicken", 340 Events: []string{"push"}, 341 }, 342 { 343 Name: "water", 344 }, 345 { 346 Name: "chocolate", 347 Events: []string{"pull_request", "issue_comment", "issues"}, 348 }, 349 }, 350 }, 351 }, 352 } 353 354 for _, test := range tests { 355 t.Run(test.name, func(t *testing.T) { 356 357 t.Logf("Running scenario %q", test.name) 358 359 pa := &plugins.ConfigAgent{} 360 pa.Set(&plugins.Configuration{ 361 ExternalPlugins: test.plugins, 362 }) 363 364 if test.repoEnabled == nil { 365 test.repoEnabled = func(_, _ string) bool { return true } 366 } 367 s := &Server{Plugins: pa, RepoEnabled: test.repoEnabled} 368 369 gotPlugins := s.needDemux(test.eventType, test.srcRepo) 370 if len(gotPlugins) != len(test.expected) { 371 t.Fatalf("expected plugins: %+v, got: %+v", test.expected, gotPlugins) 372 } 373 for _, expected := range test.expected { 374 var found bool 375 for _, got := range gotPlugins { 376 if got.Name != expected.Name { 377 continue 378 } 379 if !reflect.DeepEqual(expected, got) { 380 t.Errorf("expected plugin: %+v, got: %+v", expected, got) 381 } 382 found = true 383 } 384 if !found { 385 t.Errorf("expected plugins: %+v, got: %+v", test.expected, gotPlugins) 386 break 387 } 388 } 389 }) 390 } 391 } 392 393 type roundTripFunc func(req *http.Request) *http.Response 394 395 // RoundTrip . 396 func (f roundTripFunc) RoundTrip(req *http.Request) (*http.Response, error) { 397 return f(req), nil 398 } 399 400 // newTestClient returns *http.Client with Transport replaced to avoid making real calls 401 func newTestClient(fn roundTripFunc) *http.Client { 402 return &http.Client{ 403 Transport: fn, 404 } 405 } 406 407 func TestDemuxEvent(t *testing.T) { 408 409 getSecret := func() []byte { 410 var repoLevelSecret = ` 411 '*': 412 - value: abc 413 created_at: 2019-10-02T15:00:00Z 414 - value: key2 415 created_at: 2020-10-02T15:00:00Z 416 foo/bar: 417 - value: 123abc 418 created_at: 2019-10-02T15:00:00Z 419 - value: key6 420 created_at: 2020-10-02T15:00:00Z 421 ` 422 return []byte(repoLevelSecret) 423 } 424 425 externalPlugins := map[string][]plugins.ExternalPlugin{ 426 "kubernetes/test-infra": { 427 { 428 Name: "coffee", 429 Endpoint: "/coffee", 430 }, 431 }, 432 "kubernetes/kubernetes": { 433 { 434 Name: "gumbo", 435 Endpoint: "/gumbo", 436 Events: []string{"issue_comment"}, 437 }, 438 }, 439 "kubernetes": { 440 { 441 Name: "chicken", 442 Endpoint: "/chicken", 443 Events: []string{"repository"}, 444 }, 445 { 446 Name: "water", 447 Endpoint: "/water", 448 }, 449 { 450 Name: "chocolate", 451 Endpoint: "/chocolate", 452 Events: []string{"pull_request", "issue_comment", "repository"}, 453 }, 454 { 455 Name: "unknown_event_handler", 456 Endpoint: "/unknown", 457 Events: []string{"unknown_event"}, 458 }, 459 }, 460 } 461 462 // This is the SHA1 signature for payload "$BODY" and signature "abc" 463 // echo -n $BODY | openssl dgst -sha1 -hmac abc 464 const hmac string = "sha1=d5f926df2d39006bdb5b6acb18f8fcdebad7a052" 465 const body string = `{ 466 "action": "edited", 467 "changes": { 468 "default_branch": { 469 "from": "master" 470 } 471 }, 472 "repository": { 473 "full_name": "kubernetes/test-infra", 474 "default_branch": "master" 475 } 476 }` 477 478 metrics := githubeventserver.NewMetrics() 479 pa := &plugins.ConfigAgent{} 480 pa.Set(&plugins.Configuration{ 481 ExternalPlugins: externalPlugins, 482 }) 483 484 var testcases = []struct { 485 name string 486 487 Method string 488 Header map[string]string 489 Body string 490 491 ExpectedDispatch []string 492 }{ 493 { 494 name: "Repository event", 495 496 Method: http.MethodPost, 497 Header: map[string]string{ 498 "X-GitHub-Event": "repository", 499 "X-GitHub-Delivery": "I am unique", 500 "X-Hub-Signature": hmac, 501 "content-type": "application/json", 502 }, 503 Body: body, 504 505 ExpectedDispatch: []string{"/coffee", "/water", "/chicken", "/chocolate"}, 506 }, 507 { 508 name: "Issue comment event", 509 510 Method: http.MethodPost, 511 Header: map[string]string{ 512 "X-GitHub-Event": "issue_comment", 513 "X-GitHub-Delivery": "I am unique", 514 "X-Hub-Signature": hmac, 515 "content-type": "application/json", 516 }, 517 Body: body, 518 519 ExpectedDispatch: []string{"/coffee", "/water", "/chocolate"}, 520 }, 521 { 522 name: "Unknown event type gets dispatched to external plugin", 523 524 Method: http.MethodPost, 525 Header: map[string]string{ 526 "X-GitHub-Event": "unknown_event", 527 "X-GitHub-Delivery": "I am unique", 528 "X-Hub-Signature": hmac, 529 "content-type": "application/json", 530 }, 531 Body: body, 532 533 ExpectedDispatch: []string{"/coffee", "/water", "/unknown"}, 534 }, 535 } 536 537 for _, tc := range testcases { 538 t.Run(tc.name, func(t *testing.T) { 539 t.Logf("Running scenario %q", tc.name) 540 541 var calledExternalPlugins []string 542 var m sync.Mutex 543 544 client := newTestClient(func(req *http.Request) *http.Response { 545 m.Lock() 546 calledExternalPlugins = append(calledExternalPlugins, req.URL.String()) 547 m.Unlock() 548 return &http.Response{ 549 StatusCode: 200, 550 Body: io.NopCloser(bytes.NewBufferString(`OK`)), 551 Header: make(http.Header), 552 } 553 }) 554 555 s := &Server{ 556 Metrics: metrics, 557 Plugins: pa, 558 TokenGenerator: getSecret, 559 RepoEnabled: func(org, repo string) bool { return true }, 560 c: *client, 561 } 562 w := httptest.NewRecorder() 563 r, err := http.NewRequest(tc.Method, "", strings.NewReader(tc.Body)) 564 if err != nil { 565 t.Fatal(err) 566 } 567 for k, v := range tc.Header { 568 r.Header.Set(k, v) 569 } 570 s.ServeHTTP(w, r) 571 s.wg.Wait() 572 573 if diff := cmp.Diff(tc.ExpectedDispatch, calledExternalPlugins, cmpopts.SortSlices(func(a, b string) bool { 574 return a < b 575 })); diff != "" { 576 t.Fatalf("Expected plugins calls mismatch. got(+), want(-):\n%s", diff) 577 } 578 }) 579 } 580 }