sigs.k8s.io/prow@v0.0.0-20240503223140-c5e374dc7eb1/pkg/plugins/cat/cat_test.go (about) 1 /* 2 Copyright 2018 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 cat 18 19 import ( 20 "errors" 21 "flag" 22 "fmt" 23 "io" 24 "net/http" 25 "net/http/httptest" 26 "regexp" 27 "strings" 28 "testing" 29 30 "github.com/sirupsen/logrus" 31 32 "sigs.k8s.io/prow/pkg/github" 33 "sigs.k8s.io/prow/pkg/github/fakegithub" 34 ) 35 36 type fakeClowder string 37 38 var human = flag.Bool("human", false, "Enable to run additional manual tests") 39 var category = flag.String("category", "", "Request a particular category if set") 40 var movieCat = flag.Bool("gif", false, "Specifically request a GIF image if set") 41 var keyPath = flag.String("key-path", "", "Path to api key if set") 42 43 func (c fakeClowder) readCat(category string, movieCat bool, grumpyRoot string) (string, error) { 44 if category == "error" { 45 return "", errors.New(string(c)) 46 } 47 return fmt.Sprintf("", c), nil 48 } 49 50 func TestRealCat(t *testing.T) { 51 if !*human { 52 t.Skip("Real cats disabled for automation. Manual users can add --human [--category=foo]") 53 } 54 if *keyPath != "" { 55 meow.setKey(*keyPath, logrus.WithField("plugin", pluginName)) 56 } 57 58 if cat, err := meow.readCat(*category, *movieCat, defaultGrumpyRoot); err != nil { 59 t.Errorf("Could not read cats from %#v: %v", meow, err) 60 } else { 61 fmt.Println(cat) 62 } 63 } 64 65 func TestUrl(t *testing.T) { 66 cases := []struct { 67 name string 68 url string 69 category string 70 key string 71 movie bool 72 require []string 73 deny []string 74 }{ 75 { 76 name: "only url", 77 url: "http://foo", 78 }, 79 { 80 name: "key", 81 url: "http://foo", 82 key: "blah", 83 require: []string{"api_key=blah"}, 84 deny: []string{"category=", "mime_types=gif"}, 85 }, 86 { 87 name: "category", 88 url: "http://foo", 89 category: "bar", 90 require: []string{"category=bar"}, 91 deny: []string{"api_key=", "mime_types=gif"}, 92 }, 93 { 94 name: "movie", 95 url: "http://foo", 96 movie: true, 97 require: []string{"mime_types=gif"}, 98 deny: []string{"category=this", "api_key=that"}, 99 }, 100 { 101 name: "category and movie", 102 url: "http://foo", 103 category: "this", 104 movie: true, 105 require: []string{"mime_types=gif", "category=this", "&"}, 106 deny: []string{"api_key="}, 107 }, 108 { 109 name: "category and key", 110 url: "http://foo", 111 category: "this", 112 key: "that", 113 require: []string{"category=this", "api_key=that", "&"}, 114 deny: []string{"mime_types=gif"}, 115 }, 116 { 117 name: "category, key, and movie", 118 url: "http://foo", 119 category: "this", 120 key: "that", 121 movie: true, 122 require: []string{"category=this", "api_key=that", "&", "mime_types=gif"}, 123 }, 124 } 125 126 for _, tc := range cases { 127 rc := realClowder{ 128 url: tc.url, 129 key: tc.key, 130 } 131 url := rc.URL(tc.category, tc.movie) 132 for _, r := range tc.require { 133 if !strings.Contains(url, r) { 134 t.Errorf("%s: %s does not contain %s", tc.name, url, r) 135 } 136 } 137 for _, d := range tc.deny { 138 if strings.Contains(url, d) { 139 t.Errorf("%s: %s contained unexpected %s", tc.name, url, d) 140 } 141 } 142 } 143 } 144 145 func TestGrumpy(t *testing.T) { 146 // fake server for images 147 ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 148 body := "binary image" 149 io.WriteString(w, body) 150 })) 151 defer ts.Close() 152 153 cases := []struct { 154 name string 155 url string 156 category string 157 key string 158 movie bool 159 require []string 160 deny []string 161 }{ 162 { 163 name: "category", 164 url: "http://foo", 165 category: "bar", 166 movie: false, 167 deny: []string{ts.URL + "/" + grumpyIMG}, 168 }, 169 { 170 name: "category and movie", 171 url: "http://foo", 172 category: "this", 173 movie: true, 174 deny: []string{ts.URL + "/" + grumpyIMG}, 175 }, 176 { 177 name: "grumpy cat no keyword", 178 url: "http://foo", 179 category: "no", 180 key: "that", 181 movie: false, 182 require: []string{ts.URL + "/" + grumpyIMG}, 183 }, 184 { 185 name: "grumpy cat grumpy keyword", 186 url: "http://foo", 187 category: "grumpy", 188 key: "that", 189 movie: false, 190 require: []string{ts.URL + "/" + grumpyIMG}, 191 }, 192 } 193 194 for _, tc := range cases { 195 rc := realClowder{ 196 url: tc.url, 197 key: tc.key, 198 } 199 url, _ := rc.readCat(tc.category, tc.movie, ts.URL+"/") 200 for _, r := range tc.require { 201 if !strings.Contains(url, r) { 202 t.Errorf("%s: %s does not contain %s", tc.name, url, r) 203 } 204 } 205 for _, d := range tc.deny { 206 if strings.Contains(url, d) { 207 t.Errorf("%s: %s contained unexpected %s", tc.name, url, d) 208 } 209 } 210 } 211 } 212 213 func TestFormat(t *testing.T) { 214 re := regexp.MustCompile(`!\[.+\]\(.+\)`) 215 basicURL := "http://example.com" 216 testcases := []struct { 217 name string 218 img string 219 err bool 220 }{ 221 { 222 name: "basically works", 223 img: basicURL, 224 err: false, 225 }, 226 { 227 name: "empty image", 228 img: "", 229 err: true, 230 }, 231 { 232 name: "bad image", 233 img: "http://still a bad url", 234 err: true, 235 }, 236 } 237 for _, tc := range testcases { 238 ret, err := catResult{ 239 Image: tc.img, 240 }.Format() 241 242 switch { 243 case tc.err: 244 if err == nil { 245 t.Errorf("%s: failed to raise an error", tc.name) 246 } 247 case err != nil: 248 t.Errorf("%s: unexpected error: %v", tc.name, err) 249 case !re.MatchString(ret): 250 t.Errorf("%s: bad return value: %s", tc.name, ret) 251 } 252 } 253 } 254 255 // Medium integration test (depends on ability to open a TCP port) 256 func TestHttpResponse(t *testing.T) { 257 // create test cases for handling content length of images 258 contentLength := make(map[string]string) 259 contentLength["/cat.jpg"] = "717987" 260 contentLength["/bigcat.jpg"] = "12647753" 261 262 // fake server for images 263 ts2 := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 264 if s, ok := contentLength[r.URL.Path]; ok { 265 body := "binary image" 266 w.Header().Set("Content-Length", s) 267 io.WriteString(w, body) 268 } else { 269 t.Errorf("Cannot find content length for %s", r.URL.Path) 270 } 271 })) 272 defer ts2.Close() 273 274 // create test cases for handling http responses 275 img := ts2.URL + "/cat.jpg" 276 bigimg := ts2.URL + "/bigcat.jpg" 277 validResponse := fmt.Sprintf(`[{"id":"valid","url":"%s"}]`, img) 278 var testcases = []struct { 279 name string 280 path string 281 response string 282 valid bool 283 code int 284 }{ 285 { 286 name: "valid", 287 path: "/valid", 288 response: validResponse, 289 valid: true, 290 }, 291 { 292 name: "image too big", 293 path: "/too-big", 294 response: fmt.Sprintf(`[{"id":"toobig","url":"%s"}]`, bigimg), 295 }, 296 { 297 name: "return-406", 298 path: "/return-406", 299 code: 406, 300 response: ` 301 <!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN"> 302 <html><head> 303 <title>406 Not Acceptable</title> 304 </head><body> 305 <h1>Not Acceptable</h1> 306 <p>An appropriate representation of the requested resource /api/images/get could not be found on this server.</p> 307 Available variants: 308 <ul> 309 <li><a href="get.php">get.php</a> , type x-mapp-php5</li> 310 </ul> 311 </body></html>`, 312 }, 313 { 314 name: "no-cats-in-json", 315 path: "/no-cats-in-json", 316 response: "[]", 317 }, 318 { 319 name: "no-image-in-json", 320 path: "/no-image-in-json", 321 response: "[{}]", 322 }, 323 } 324 325 // fake server for image urls 326 pathToResponse := make(map[string]string) 327 for _, testcase := range testcases { 328 pathToResponse[testcase.path] = testcase.response 329 } 330 codes := make(map[string]int) 331 for _, tc := range testcases { 332 codes[tc.path] = tc.code 333 } 334 ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 335 code := codes[r.URL.Path] 336 if code > 0 { 337 w.WriteHeader(code) 338 } 339 if r, ok := pathToResponse[r.URL.Path]; ok { 340 io.WriteString(w, r) 341 } else { 342 io.WriteString(w, validResponse) 343 } 344 })) 345 defer ts.Close() 346 347 // github fake client 348 fc := fakegithub.NewFakeClient() 349 fc.IssueComments = make(map[int][]github.IssueComment) 350 351 // run test for each case 352 for _, testcase := range testcases { 353 fakemeow := &realClowder{url: ts.URL + testcase.path} 354 cat, err := fakemeow.readCat(*category, *movieCat, "") 355 if testcase.valid && err != nil { 356 t.Errorf("For case %s, didn't expect error: %v", testcase.name, err) 357 } else if !testcase.valid && err == nil { 358 t.Errorf("For case %s, expected error, received cat: %s", testcase.name, cat) 359 } else if testcase.valid && cat == "" { 360 t.Errorf("For case %s, got an empty cat", testcase.name) 361 } 362 } 363 364 // fully test handling a comment 365 comment := "/meowvie space" 366 367 e := &github.GenericCommentEvent{ 368 Action: github.GenericCommentActionCreated, 369 Body: comment, 370 Number: 5, 371 IssueState: "open", 372 } 373 if err := handle(fc, logrus.WithField("plugin", pluginName), e, &realClowder{url: ts.URL + "/?format=json"}, func() {}); err != nil { 374 t.Errorf("didn't expect error: %v", err) 375 return 376 } 377 if len(fc.IssueComments[5]) != 1 { 378 t.Error("should have commented.") 379 return 380 } 381 if c := fc.IssueComments[5][0]; !strings.Contains(c.Body, img) { 382 t.Errorf("missing image url: %s from comment: %v", img, c) 383 } 384 385 } 386 387 // Small, unit tests 388 func TestCats(t *testing.T) { 389 var testcases = []struct { 390 name string 391 action github.GenericCommentEventAction 392 body string 393 state string 394 pr bool 395 shouldComment bool 396 shouldError bool 397 }{ 398 { 399 name: "ignore edited comment", 400 state: "open", 401 action: github.GenericCommentActionEdited, 402 body: "/meow", 403 shouldComment: false, 404 shouldError: false, 405 }, 406 { 407 name: "leave cat on pr", 408 state: "open", 409 action: github.GenericCommentActionCreated, 410 body: "/meow", 411 pr: true, 412 shouldComment: true, 413 shouldError: false, 414 }, 415 { 416 name: "leave cat on issue", 417 state: "open", 418 action: github.GenericCommentActionCreated, 419 body: "/meow", 420 shouldComment: true, 421 shouldError: false, 422 }, 423 { 424 name: "leave cat on issue, trailing space", 425 state: "open", 426 action: github.GenericCommentActionCreated, 427 body: "/meow \r", 428 shouldComment: true, 429 shouldError: false, 430 }, 431 { 432 name: "categorical cat", 433 state: "open", 434 action: github.GenericCommentActionCreated, 435 body: "/meow clothes", 436 shouldComment: true, 437 shouldError: false, 438 }, 439 { 440 name: "bad cat", 441 state: "open", 442 action: github.GenericCommentActionCreated, 443 body: "/meow error", 444 shouldComment: true, 445 shouldError: true, 446 }, 447 { 448 name: "movie cat", 449 state: "open", 450 action: github.GenericCommentActionCreated, 451 body: "/meowvie", 452 shouldComment: true, 453 shouldError: false, 454 }, 455 { 456 name: "categorical movie cat", 457 state: "open", 458 action: github.GenericCommentActionCreated, 459 body: "/meowvie space", 460 shouldComment: true, 461 shouldError: false, 462 }, 463 } 464 for _, tc := range testcases { 465 fc := fakegithub.NewFakeClient() 466 fc.IssueComments = make(map[int][]github.IssueComment) 467 e := &github.GenericCommentEvent{ 468 Action: tc.action, 469 Body: tc.body, 470 Number: 5, 471 IssueState: tc.state, 472 IsPR: tc.pr, 473 } 474 err := handle(fc, logrus.WithField("plugin", pluginName), e, fakeClowder("tubbs"), func() {}) 475 if !tc.shouldError && err != nil { 476 t.Errorf("%s: didn't expect error: %v", tc.name, err) 477 continue 478 } else if tc.shouldError && err == nil { 479 t.Errorf("%s: expected an error to occur", tc.name) 480 continue 481 } 482 if tc.shouldComment && len(fc.IssueComments[5]) != 1 { 483 t.Errorf("%s: should have commented.", tc.name) 484 } else if tc.shouldComment { 485 shouldImage := !tc.shouldError 486 body := fc.IssueComments[5][0].Body 487 hasImage := strings.Contains(body, "![") 488 if hasImage && !shouldImage { 489 t.Errorf("%s: unexpected image in %s", tc.name, body) 490 } else if !hasImage && shouldImage { 491 t.Errorf("%s: no image in %s", tc.name, body) 492 } 493 } else if !tc.shouldComment && len(fc.IssueComments[5]) != 0 { 494 t.Errorf("%s: should not have commented.", tc.name) 495 } 496 } 497 }