sigs.k8s.io/prow@v0.0.0-20240503223140-c5e374dc7eb1/pkg/plugins/pony/pony_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 pony 18 19 import ( 20 "encoding/json" 21 "flag" 22 "fmt" 23 "io" 24 "net/http" 25 "net/http/httptest" 26 "strings" 27 "testing" 28 29 "github.com/sirupsen/logrus" 30 "sigs.k8s.io/prow/pkg/github" 31 "sigs.k8s.io/prow/pkg/github/fakegithub" 32 ) 33 34 type fakeHerd string 35 36 var human = flag.Bool("human", false, "Enable to run additional manual tests") 37 var ponyFlag = flag.String("pony", "", "Request a particular pony if set") 38 39 func (c fakeHerd) readPony(tags string) (string, error) { 40 if tags != "" { 41 return tags, nil 42 } 43 return string(c), nil 44 } 45 46 func parsePoniesFromComment(comment []github.IssueComment) (ponies int) { 47 if comment == nil { 48 return 49 } 50 // Golang doesn't support lookback regex matches. Hence this hack to parse the pony URLs from rest of the comment. 51 var rawComment = comment[0].Body 52 rawComment = rawComment[strings.Index(rawComment, ":")+1:] 53 rawComment = strings.TrimSpace(rawComment[:strings.Index(rawComment, "<details>")]) 54 return len(strings.Split(rawComment, "\n")) 55 } 56 57 func TestRealPony(t *testing.T) { 58 if !*human { 59 t.Skip("Real ponies disabled for automation. Manual users can add --human [--category=foo]") 60 } 61 if pony, err := ponyURL.readPony(*ponyFlag); err != nil { 62 t.Errorf("Could not read pony from %s: %v", ponyURL, err) 63 } else { 64 fmt.Println(pony) 65 } 66 } 67 68 func TestFormat(t *testing.T) { 69 result := formatURLs("http://example.com/small", "http://example.com/full") 70 expected := "[](http://example.com/full)" 71 if result != expected { 72 t.Errorf("Expected %q, but got %q", expected, result) 73 } 74 } 75 76 // Medium integration test (depends on ability to open a TCP port) 77 func TestHttpResponse(t *testing.T) { 78 79 // create test cases for handling content length of images 80 contentLength := make(map[string]string) 81 contentLength["/pony.jpg"] = "717987" 82 contentLength["/horse.png"] = "12647753" 83 84 // fake server for images 85 ts2 := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 86 if r.URL.Path == "/full" { 87 t.Errorf("Requested full-size image instead of small image.") 88 http.NotFound(w, r) 89 return 90 } 91 if s, ok := contentLength[r.URL.Path]; ok { 92 body := "binary image" 93 w.Header().Set("Content-Length", s) 94 io.WriteString(w, body) 95 } else { 96 t.Errorf("Cannot find content length for %s", r.URL.Path) 97 } 98 })) 99 defer ts2.Close() 100 101 // setup a stock valid request 102 url := ts2.URL + "/pony.jpg" 103 b, err := json.Marshal(&ponyResult{ 104 Pony: ponyResultPony{ 105 Representations: ponyRepresentations{ 106 Small: ts2.URL + "/pony.jpg", 107 Full: ts2.URL + "/full", 108 }, 109 }, 110 }) 111 if err != nil { 112 t.Errorf("Failed to encode test data: %v", err) 113 } 114 115 // create test cases for handling http responses 116 validResponse := string(b) 117 118 type testcase struct { 119 name string 120 comment string 121 path string 122 response string 123 expected string 124 expectTag string 125 expectNoTag bool 126 isValid bool 127 noPony bool 128 } 129 130 var testcases = []testcase{ 131 { 132 name: "valid", 133 comment: "/pony", 134 path: "/valid", 135 response: validResponse, 136 expected: url, 137 isValid: true, 138 }, 139 { 140 name: "no pony found", 141 comment: "/pony", 142 path: "/404", 143 noPony: true, 144 isValid: false, 145 }, 146 { 147 name: "invalid JSON", 148 comment: "/pony", 149 path: "/bad-json", 150 response: `{"bad-blob": "not-a-url"`, 151 isValid: false, 152 }, 153 { 154 name: "image too big", 155 comment: "/pony", 156 path: "/too-big", 157 response: fmt.Sprintf(`{"pony":{"representations": {"small": "%s/horse.png", "full": "%s/full"}}}`, ts2.URL, ts2.URL), 158 isValid: false, 159 }, 160 { 161 name: "has tag", 162 comment: "/pony peach hack", 163 path: "/peach", 164 isValid: true, 165 expectTag: "peach hack", 166 response: validResponse, 167 }, 168 { 169 name: "pony embedded in other commands", 170 comment: "/meow\n/pony\n/woof\n\nTesting :)", 171 path: "/embedded", 172 isValid: true, 173 expectNoTag: true, 174 response: validResponse, 175 }, 176 } 177 178 // fake server for image urls 179 pathToTestCase := make(map[string]*testcase) 180 for _, testcase := range testcases { 181 tc := testcase 182 pathToTestCase[testcase.path] = &tc 183 } 184 ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 185 if tc, ok := pathToTestCase[r.URL.Path]; ok { 186 if tc.noPony { 187 http.NotFound(w, r) 188 return 189 } 190 q := r.URL.Query().Get("q") 191 if tc.expectTag != "" && q != tc.expectTag { 192 t.Errorf("Expected tag %q, but got %q", tc.expectTag, q) 193 } 194 if tc.expectNoTag && q != "" { 195 t.Errorf("Expected no tag, but got %q", q) 196 } 197 io.WriteString(w, tc.response) 198 } else { 199 io.WriteString(w, validResponse) 200 } 201 })) 202 defer ts.Close() 203 204 // run test for each case 205 for _, testcase := range testcases { 206 pony, err := realHerd(ts.URL + testcase.path).readPony(testcase.expectTag) 207 if testcase.isValid && err != nil { 208 t.Errorf("For case %s, didn't expect error: %v", testcase.name, err) 209 } else if !testcase.isValid && err == nil { 210 t.Errorf("For case %s, expected error, received pony: %s", testcase.name, pony) 211 } 212 213 if !testcase.isValid { 214 continue 215 } 216 217 // github fake client 218 fc := fakegithub.NewFakeClient() 219 220 // fully test handling a comment 221 e := &github.GenericCommentEvent{ 222 Action: github.GenericCommentActionCreated, 223 Body: testcase.comment, 224 Number: 5, 225 IssueState: "open", 226 } 227 err = handle(fc, logrus.WithField("plugin", pluginName), e, realHerd(ts.URL+testcase.path)) 228 if err != nil { 229 t.Errorf("tc %s: For comment %s, didn't expect error: %v", testcase.name, testcase.comment, err) 230 } 231 232 if len(fc.IssueComments[5]) != 1 { 233 t.Errorf("tc %s: should have commented", testcase.name) 234 } 235 if c := fc.IssueComments[5][0]; !strings.Contains(c.Body, testcase.expected) { 236 t.Errorf("tc %s: missing image url: %s from comment: %v", testcase.name, testcase.expected, c.Body) 237 } 238 } 239 } 240 241 // Small, unit tests 242 func TestPonies(t *testing.T) { 243 var testcases = []struct { 244 name string 245 action github.GenericCommentEventAction 246 body string 247 state string 248 pr bool 249 numPonies int 250 }{ 251 { 252 name: "ignore edited comment", 253 state: "open", 254 action: github.GenericCommentActionEdited, 255 body: "/pony", 256 numPonies: 0, 257 }, 258 { 259 name: "leave pony on pr", 260 state: "open", 261 action: github.GenericCommentActionCreated, 262 body: "/pony", 263 pr: true, 264 numPonies: 1, 265 }, 266 { 267 name: "leave pony on issue", 268 state: "open", 269 action: github.GenericCommentActionCreated, 270 body: "/pony", 271 numPonies: 1, 272 }, 273 { 274 name: "leave pony on issue, trailing space", 275 state: "open", 276 action: github.GenericCommentActionCreated, 277 body: "/pony \r", 278 numPonies: 1, 279 }, 280 { 281 name: "leave pony on issue, tag specified", 282 state: "open", 283 action: github.GenericCommentActionCreated, 284 body: "/pony Twilight Sparkle", 285 numPonies: 1, 286 }, 287 { 288 name: "leave pony on issue, tag specified, trailing space", 289 state: "open", 290 action: github.GenericCommentActionCreated, 291 body: "/pony Twilight Sparkle \r", 292 numPonies: 1, 293 }, 294 { 295 name: "leave multiple ponies on issue, mixed tags specified, trailing space", 296 state: "open", 297 action: github.GenericCommentActionCreated, 298 body: "/pony one \n/pony \n/pony three \n/pony \n", 299 numPonies: 4, 300 }, 301 { 302 name: "More than N ponies on issue but only N are picked", 303 state: "open", 304 action: github.GenericCommentActionCreated, 305 body: "/pony one \n/pony two \n/pony three \n/pony four \n/pony five \n/pony six", 306 numPonies: 5, 307 }, 308 { 309 name: "don't leave cats or dogs", 310 state: "open", 311 action: github.GenericCommentActionCreated, 312 body: "/woof\n/meow", 313 numPonies: 0, 314 }, 315 { 316 name: "do nothing in the middle of a line", 317 state: "open", 318 action: github.GenericCommentActionCreated, 319 body: "did you know that /pony makes ponies happen?", 320 numPonies: 0, 321 }, 322 } 323 for _, tc := range testcases { 324 fc := fakegithub.NewFakeClient() 325 e := &github.GenericCommentEvent{ 326 Action: tc.action, 327 Body: tc.body, 328 Number: 5, 329 IssueState: tc.state, 330 IsPR: tc.pr, 331 } 332 err := handle(fc, logrus.WithField("plugin", pluginName), e, fakeHerd("pone")) 333 if err != nil { 334 t.Errorf("For case %s, didn't expect error: %v", tc.name, err) 335 } 336 337 var actualPonyCount = parsePoniesFromComment(fc.IssueComments[5]) 338 if tc.numPonies != actualPonyCount { 339 t.Errorf("For case '%s', #expected ponies %v, #found ponies %v", tc.name, tc.numPonies, actualPonyCount) 340 } 341 } 342 }