sigs.k8s.io/prow@v0.0.0-20240503223140-c5e374dc7eb1/pkg/bugzilla/client_test.go (about) 1 /* 2 Copyright 2019 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 bugzilla 18 19 import ( 20 "crypto/tls" 21 "encoding/json" 22 "fmt" 23 "io" 24 "net/http" 25 "net/http/httptest" 26 "regexp" 27 "strconv" 28 "strings" 29 "testing" 30 "time" 31 32 "github.com/google/go-cmp/cmp" 33 "github.com/sirupsen/logrus" 34 "k8s.io/apimachinery/pkg/util/diff" 35 "k8s.io/apimachinery/pkg/util/sets" 36 ) 37 38 var ( 39 bugData = []byte(`{"bugs":[{"alias":[],"assigned_to":"Steve Kuznetsov","assigned_to_detail":{"email":"skuznets","id":381851,"name":"skuznets","real_name":"Steve Kuznetsov"},"blocks":[],"cc":["Sudha Ponnaganti"],"cc_detail":[{"email":"sponnaga","id":426940,"name":"sponnaga","real_name":"Sudha Ponnaganti"}],"classification":"Red Hat","component":["Test Infrastructure"],"creation_time":"2019-05-01T19:33:36Z","creator":"Dan Mace","creator_detail":{"email":"dmace","id":330250,"name":"dmace","real_name":"Dan Mace"},"deadline":null,"depends_on":[],"docs_contact":"","dupe_of":null,"groups":[],"id":1705243,"is_cc_accessible":true,"is_confirmed":true,"is_creator_accessible":true,"is_open":true,"keywords":[],"last_change_time":"2019-05-17T15:13:13Z","op_sys":"Unspecified","platform":"Unspecified","priority":"unspecified","product":"OpenShift Container Platform","qa_contact":"","resolution":"","see_also":[],"severity":"medium","status":"VERIFIED","summary":"[ci] cli image flake affecting *-images jobs","target_milestone":"---","target_release":["3.11.z"],"url":"","version":["3.11.0"],"whiteboard":""}],"faults":[]}`) 40 bugStruct = &Bug{Alias: []string{}, AssignedTo: "Steve Kuznetsov", AssignedToDetail: &User{Email: "skuznets", ID: 381851, Name: "skuznets", RealName: "Steve Kuznetsov"}, Blocks: []int{}, CC: []string{"Sudha Ponnaganti"}, CCDetail: []User{{Email: "sponnaga", ID: 426940, Name: "sponnaga", RealName: "Sudha Ponnaganti"}}, Classification: "Red Hat", Component: []string{"Test Infrastructure"}, CreationTime: "2019-05-01T19:33:36Z", Creator: "Dan Mace", CreatorDetail: &User{Email: "dmace", ID: 330250, Name: "dmace", RealName: "Dan Mace"}, DependsOn: []int{}, ID: 1705243, IsCCAccessible: true, IsConfirmed: true, IsCreatorAccessible: true, IsOpen: true, Groups: []string{}, Keywords: []string{}, LastChangeTime: "2019-05-17T15:13:13Z", OperatingSystem: "Unspecified", Platform: "Unspecified", Priority: "unspecified", Product: "OpenShift Container Platform", SeeAlso: []string{}, Severity: "medium", Status: "VERIFIED", Summary: "[ci] cli image flake affecting *-images jobs", TargetRelease: []string{"3.11.z"}, TargetMilestone: "---", Version: []string{"3.11.0"}} 41 bugAccessDenied = []byte(`{"error":true,"code":102,"message":"You are not authorized to access bug #2. To see this bug, you must first log in to an account with the appropriate permissions."}`) 42 bugInvalidBugID = []byte(`{"error":true,"code":101,"message":"Bug #3 does not exist."}`) 43 ) 44 45 func clientForUrl(url string) *client { 46 return &client{ 47 logger: logrus.WithField("testing", "true"), 48 delegate: &delegate{ 49 authMethod: "x-bugzilla-api-key", 50 endpoint: url, 51 client: &http.Client{ 52 Transport: &http.Transport{ 53 TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, 54 }, 55 }, 56 getAPIKey: func() []byte { 57 return []byte("api-key") 58 }, 59 }, 60 } 61 } 62 63 func TestGetBug(t *testing.T) { 64 testServer := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 65 if r.Header.Get("X-BUGZILLA-API-KEY") != "api-key" { 66 t.Error("did not get api-key passed in X-BUGZILLA-API-KEY header") 67 http.Error(w, "403 Forbidden", http.StatusForbidden) 68 return 69 } 70 if r.Method != http.MethodGet { 71 t.Errorf("incorrect method to get a bug: %s", r.Method) 72 http.Error(w, "400 Bad Request", http.StatusBadRequest) 73 return 74 } 75 if !strings.HasPrefix(r.URL.Path, "/rest/bug/") { 76 t.Errorf("incorrect path to get a bug: %s", r.URL.Path) 77 http.Error(w, "400 Bad Request", http.StatusBadRequest) 78 return 79 } 80 if id, err := strconv.Atoi(strings.TrimPrefix(r.URL.Path, "/rest/bug/")); err != nil { 81 t.Errorf("malformed bug id: %s", r.URL.Path) 82 http.Error(w, "400 Bad Request", http.StatusBadRequest) 83 return 84 } else { 85 if id == 1705243 { 86 w.Write(bugData) 87 } else if id == 2 { 88 w.Write(bugAccessDenied) 89 } else if id == 3 { 90 w.Write(bugInvalidBugID) 91 } else { 92 http.Error(w, "404 Not Found", http.StatusNotFound) 93 } 94 } 95 })) 96 defer testServer.Close() 97 client := clientForUrl(testServer.URL) 98 99 // this should give us what we want 100 bug, err := client.GetBug(1705243) 101 if err != nil { 102 t.Errorf("expected no error, but got one: %v", err) 103 } 104 if diff := cmp.Diff(bug, bugStruct); diff != "" { 105 t.Errorf("got incorrect bug: %v", diff) 106 } 107 108 // this should 404 109 otherBug, err := client.GetBug(1) 110 if err == nil { 111 t.Error("expected an error, but got none") 112 } else if !IsNotFound(err) { 113 t.Errorf("expected a not found error, got %v", err) 114 } 115 if otherBug != nil { 116 t.Errorf("expected no bug, got: %v", otherBug) 117 } 118 119 // this should return access denied 120 accessDeniedBug, err := client.GetBug(2) 121 if err == nil { 122 t.Error("expected an error, but got none") 123 } else if !IsAccessDenied(err) { 124 t.Errorf("expected an access denied error, got %v", err) 125 } 126 if accessDeniedBug != nil { 127 t.Errorf("expected no bug, got: %v", accessDeniedBug) 128 } 129 130 // this should return invalid Bug ID 131 invalidIDBug, err := client.GetBug(3) 132 if err == nil { 133 t.Error("expected an error, but got none") 134 } else if !IsInvalidBugID(err) { 135 t.Errorf("expected an invalid bug error, got %v", err) 136 } 137 if invalidIDBug != nil { 138 t.Errorf("expected no bug, got: %v", invalidIDBug) 139 } 140 } 141 142 func TestCreateBug(t *testing.T) { 143 testServer := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 144 if r.Header.Get("X-BUGZILLA-API-KEY") != "api-key" { 145 t.Error("did not get api-key passed in X-BUGZILLA-API-KEY header") 146 http.Error(w, "403 Forbidden", http.StatusForbidden) 147 return 148 } 149 if r.Method != http.MethodPost { 150 t.Errorf("incorrect method to create a bug: %s", r.Method) 151 http.Error(w, "400 Bad Request", http.StatusBadRequest) 152 return 153 } 154 if !strings.HasPrefix(r.URL.Path, "/rest/bug") { 155 t.Errorf("incorrect path to create a bug: %s", r.URL.Path) 156 http.Error(w, "400 Bad Request", http.StatusBadRequest) 157 return 158 } 159 raw, err := io.ReadAll(r.Body) 160 if err != nil { 161 t.Errorf("failed to read request body: %v", err) 162 http.Error(w, "500 Server Error", http.StatusInternalServerError) 163 return 164 } 165 payload := &BugCreate{} 166 if err := json.Unmarshal(raw, &payload); err != nil { 167 t.Errorf("malformed JSONRPC payload: %s", string(raw)) 168 http.Error(w, "400 Bad Request", http.StatusBadRequest) 169 return 170 } 171 if _, err := w.Write([]byte(`{"id" : 12345}`)); err != nil { 172 t.Fatalf("failed to send JSONRPC response: %v", err) 173 } 174 })) 175 defer testServer.Close() 176 client := clientForUrl(testServer.URL) 177 178 // this should create a new bug 179 if id, err := client.CreateBug(&BugCreate{Description: "This is a test bug"}); err != nil { 180 t.Errorf("expected no error, but got one: %v", err) 181 } else if id != 12345 { 182 t.Errorf("expected id of 12345, got %d", id) 183 } 184 } 185 186 func TestGetComments(t *testing.T) { 187 commentsJSON := []byte(`{ 188 "bugs": { 189 "12345": { 190 "comments": [ 191 { 192 "time": "2020-04-21T13:50:04Z", 193 "text": "test bug to fix problem in removing from cc list.", 194 "bug_id": 12345, 195 "count": 0, 196 "attachment_id": null, 197 "is_private": false, 198 "is_markdown" : true, 199 "tags": [], 200 "creator": "user@bugzilla.org", 201 "creation_time": "2020-04-21T13:50:04Z", 202 "id": 75 203 }, 204 { 205 "time": "2020-04-21T13:52:02Z", 206 "text": "Bug appears to be fixed", 207 "bug_id": 12345, 208 "count": 1, 209 "attachment_id": null, 210 "is_private": false, 211 "is_markdown" : true, 212 "tags": [], 213 "creator": "user2@bugzilla.org", 214 "creation_time": "2020-04-21T13:52:02Z", 215 "id": 76 216 } 217 ] 218 } 219 }, 220 "comments": {} 221 }`) 222 commentsStruct := []Comment{{ 223 ID: 75, 224 BugID: 12345, 225 AttachmentID: nil, 226 Count: 0, 227 Text: "test bug to fix problem in removing from cc list.", 228 Creator: "user@bugzilla.org", 229 Time: time.Date(2020, time.April, 21, 13, 50, 04, 0, time.UTC), 230 CreationTime: time.Date(2020, time.April, 21, 13, 50, 04, 0, time.UTC), 231 IsPrivate: false, 232 IsMarkdown: true, 233 Tags: []string{}, 234 }, { 235 ID: 76, 236 BugID: 12345, 237 AttachmentID: nil, 238 Count: 1, 239 Text: "Bug appears to be fixed", 240 Creator: "user2@bugzilla.org", 241 Time: time.Date(2020, time.April, 21, 13, 52, 02, 0, time.UTC), 242 CreationTime: time.Date(2020, time.April, 21, 13, 52, 02, 0, time.UTC), 243 IsPrivate: false, 244 IsMarkdown: true, 245 Tags: []string{}, 246 }} 247 testServer := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 248 if r.Header.Get("X-BUGZILLA-API-KEY") != "api-key" { 249 t.Error("did not get api-key passed in X-BUGZILLA-API-KEY header") 250 http.Error(w, "403 Forbidden", http.StatusForbidden) 251 return 252 } 253 if r.Method != http.MethodGet { 254 t.Errorf("incorrect method to get bug comments: %s", r.Method) 255 http.Error(w, "400 Bad Request", http.StatusBadRequest) 256 return 257 } 258 if !strings.HasPrefix(r.URL.Path, "/rest/bug/") { 259 t.Errorf("incorrect path to get bug comments: %s", r.URL.Path) 260 http.Error(w, "400 Bad Request", http.StatusBadRequest) 261 return 262 } 263 if !strings.HasSuffix(r.URL.Path, "/comment") { 264 t.Errorf("incorrect path to get bug comments: %s", r.URL.Path) 265 http.Error(w, "400 Bad Request", http.StatusBadRequest) 266 return 267 } 268 if id, err := strconv.Atoi(strings.TrimSuffix(strings.TrimPrefix(r.URL.Path, "/rest/bug/"), "/comment")); err != nil { 269 t.Errorf("malformed bug id: %s", r.URL.Path) 270 http.Error(w, "400 Bad Request", http.StatusBadRequest) 271 } else { 272 if id == 12345 { 273 w.Write(commentsJSON) 274 } else if id == 2 { 275 w.Write(bugAccessDenied) 276 } else if id == 3 { 277 w.Write(bugInvalidBugID) 278 } else { 279 http.Error(w, "404 Not Found", http.StatusNotFound) 280 } 281 } 282 })) 283 defer testServer.Close() 284 client := clientForUrl(testServer.URL) 285 286 comments, err := client.GetComments(12345) 287 if err != nil { 288 t.Errorf("expected no error, but got one: %v", err) 289 } 290 if diff := cmp.Diff(comments, commentsStruct); diff != "" { 291 t.Errorf("got incorrect comments: %v", diff) 292 } 293 294 // this should 404 295 otherBug, err := client.GetComments(1) 296 if err == nil { 297 t.Error("expected an error, but got none") 298 } else if !IsNotFound(err) { 299 t.Errorf("expected a not found error, got %v", err) 300 } 301 if otherBug != nil { 302 t.Errorf("expected no bug, got: %v", otherBug) 303 } 304 305 // this should return access denied 306 accessDeniedBug, err := client.GetComments(2) 307 if err == nil { 308 t.Error("expected an error, but got none") 309 } else if !IsAccessDenied(err) { 310 t.Errorf("expected an access denied error, got %v", err) 311 } 312 if accessDeniedBug != nil { 313 t.Errorf("expected no bug, got: %v", accessDeniedBug) 314 } 315 316 // this should return invalid Bug ID 317 invalidIDBug, err := client.GetComments(3) 318 if err == nil { 319 t.Error("expected an error, but got none") 320 } else if !IsInvalidBugID(err) { 321 t.Errorf("expected an invalid bug error, got %v", err) 322 } 323 if invalidIDBug != nil { 324 t.Errorf("expected no bug, got: %v", invalidIDBug) 325 } 326 } 327 328 func TestCreateComment(t *testing.T) { 329 testServer := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 330 if r.Header.Get("X-BUGZILLA-API-KEY") != "api-key" { 331 t.Error("did not get api-key passed in X-BUGZILLA-API-KEY header") 332 http.Error(w, "403 Forbidden", http.StatusForbidden) 333 return 334 } 335 if r.Method != http.MethodPost { 336 t.Errorf("incorrect method to create a comment: %s", r.Method) 337 http.Error(w, "400 Bad Request", http.StatusBadRequest) 338 return 339 } 340 if !regexp.MustCompile(`^/rest/bug/\d+/comment$`).MatchString(r.URL.Path) { 341 t.Errorf("incorrect path to create a comment: %s", r.URL.Path) 342 http.Error(w, "400 Bad Request", http.StatusBadRequest) 343 return 344 } 345 raw, err := io.ReadAll(r.Body) 346 if err != nil { 347 t.Errorf("failed to read request body: %v", err) 348 http.Error(w, "500 Server Error", http.StatusInternalServerError) 349 return 350 } 351 payload := &CommentCreate{} 352 if err := json.Unmarshal(raw, &payload); err != nil { 353 t.Errorf("malformed JSONRPC payload: %s", string(raw)) 354 http.Error(w, "400 Bad Request", http.StatusBadRequest) 355 return 356 } 357 if _, err := w.Write([]byte(`{"id" : 12345}`)); err != nil { 358 t.Fatalf("failed to send JSONRPC response: %v", err) 359 } 360 })) 361 defer testServer.Close() 362 client := clientForUrl(testServer.URL) 363 364 // this should create a new comment 365 if id, err := client.CreateComment(&CommentCreate{ID: 2, Comment: "This is a test bug"}); err != nil { 366 t.Errorf("expected no error, but got one: %v", err) 367 } else if id != 12345 { 368 t.Errorf("expected id of 12345, got %d", id) 369 } 370 } 371 372 func TestCloneBugStruct(t *testing.T) { 373 testCases := []struct { 374 name string 375 bug Bug 376 comments []Comment 377 targetRelease []string 378 expected BugCreate 379 }{{ 380 name: "Clone bug", 381 bug: Bug{ 382 Alias: []string{"this_is_an_alias"}, 383 AssignedTo: "user@example.com", 384 CC: []string{"user2@example.com", "user3@example.com"}, 385 Component: []string{"TestComponent"}, 386 Flags: []Flag{{ID: 1, Name: "Test Flag"}}, 387 Groups: []string{"group1"}, 388 ID: 123, 389 Keywords: []string{"segfault"}, 390 OperatingSystem: "Fedora", 391 Platform: "x86_64", 392 Priority: "unspecified", 393 Product: "testing product", 394 QAContact: "user3@example.com", 395 Resolution: "FIXED", 396 Severity: "Urgent", 397 Status: "VERIFIED", 398 Summary: "Segfault when opening program", 399 TargetMilestone: "milestone1", 400 Version: []string{"31"}, 401 }, 402 comments: []Comment{{ 403 Text: "There is a segfault that occurs when opening applications.", 404 IsPrivate: true, 405 }}, 406 expected: BugCreate{ 407 Alias: []string{"this_is_an_alias"}, 408 AssignedTo: "user@example.com", 409 CC: []string{"user2@example.com", "user3@example.com"}, 410 Component: []string{"TestComponent"}, 411 Flags: []Flag{{ID: 1, Name: "Test Flag"}}, 412 Groups: []string{"group1"}, 413 Keywords: []string{"segfault"}, 414 OperatingSystem: "Fedora", 415 Platform: "x86_64", 416 Priority: "unspecified", 417 Product: "testing product", 418 QAContact: "user3@example.com", 419 Severity: "Urgent", 420 Summary: "Segfault when opening program", 421 TargetMilestone: "milestone1", 422 Version: []string{"31"}, 423 Description: "+++ This bug was initially created as a clone of Bug #123 +++\n\nThere is a segfault that occurs when opening applications.", 424 CommentIsPrivate: true, 425 }, 426 }, { 427 name: "Clone bug with multiple comments", 428 bug: Bug{ 429 ID: 123, 430 }, 431 comments: []Comment{{ 432 Text: "There is a segfault that occurs when opening applications.", 433 }, { 434 Text: "This is another comment.", 435 Time: time.Date(2020, time.May, 7, 2, 3, 4, 0, time.UTC), 436 CreationTime: time.Date(2020, time.May, 7, 2, 3, 4, 0, time.UTC), 437 Tags: []string{"description"}, 438 Creator: "Test Commenter", 439 }}, 440 expected: BugCreate{ 441 Description: `+++ This bug was initially created as a clone of Bug #123 +++ 442 443 There is a segfault that occurs when opening applications. 444 445 --- Additional comment from Test Commenter on 2020-05-07 02:03:04 UTC --- 446 447 This is another comment.`, 448 }, 449 }, { 450 name: "Clone bug with one private comments", 451 bug: Bug{ 452 ID: 123, 453 }, 454 comments: []Comment{{ 455 Text: "There is a segfault that occurs when opening applications.", 456 }, { 457 Text: "This is another comment.", 458 Time: time.Date(2020, time.May, 7, 2, 3, 4, 0, time.UTC), 459 CreationTime: time.Date(2020, time.May, 7, 2, 3, 4, 0, time.UTC), 460 IsPrivate: true, 461 Tags: []string{"description"}, 462 Creator: "Test Commenter", 463 }}, 464 expected: BugCreate{ 465 Description: `+++ This bug was initially created as a clone of Bug #123 +++ 466 467 There is a segfault that occurs when opening applications. 468 469 --- Additional comment from Test Commenter on 2020-05-07 02:03:04 UTC --- 470 471 This is another comment.`, 472 CommentIsPrivate: true, 473 }, 474 }} 475 for _, testCase := range testCases { 476 newBug := cloneBugStruct(&testCase.bug, nil, testCase.comments) 477 if diff := cmp.Diff(*newBug, testCase.expected); diff != "" { 478 t.Errorf("%s: Difference in expected BugCreate and actual: %s", testCase.name, diff) 479 } 480 } 481 // test max length truncation 482 bug := Bug{} 483 baseComment := Comment{Text: "This is a test comment"} 484 comments := []Comment{} 485 // Make sure comments are at lest 65535 in total length 486 for i := 0; i < (65535 / len(baseComment.Text)); i++ { 487 comments = append(comments, baseComment) 488 } 489 newBug := cloneBugStruct(&bug, nil, comments) 490 if len(newBug.Description) != 65535 { 491 t.Errorf("Truncation error in cloneBug; expected description length of 65535, got %d", len(newBug.Description)) 492 } 493 } 494 495 func TestUpdateBug(t *testing.T) { 496 testServer := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 497 if r.Header.Get("X-BUGZILLA-API-KEY") != "api-key" { 498 t.Error("did not get api-key passed in X-BUGZILLA-API-KEY header") 499 http.Error(w, "403 Forbidden", http.StatusForbidden) 500 return 501 } 502 if r.Header.Get("Content-Type") != "application/json" { 503 t.Error("did not correctly set content-type header for JSON") 504 http.Error(w, "403 Forbidden", http.StatusForbidden) 505 return 506 } 507 if r.Method != http.MethodPut { 508 t.Errorf("incorrect method to update a bug: %s", r.Method) 509 http.Error(w, "400 Bad Request", http.StatusBadRequest) 510 return 511 } 512 if !strings.HasPrefix(r.URL.Path, "/rest/bug/") { 513 t.Errorf("incorrect path to update a bug: %s", r.URL.Path) 514 http.Error(w, "400 Bad Request", http.StatusBadRequest) 515 return 516 } 517 if id, err := strconv.Atoi(strings.TrimPrefix(r.URL.Path, "/rest/bug/")); err != nil { 518 t.Errorf("malformed bug id: %s", r.URL.Path) 519 http.Error(w, "400 Bad Request", http.StatusBadRequest) 520 return 521 } else { 522 if id == 1705243 { 523 raw, err := io.ReadAll(r.Body) 524 if err != nil { 525 t.Errorf("failed to read update body: %v", err) 526 } 527 if actual, expected := string(raw), `{"depends_on":{"add":[1705242]},"status":"UPDATED"}`; actual != expected { 528 t.Errorf("got incorrect update: expected %v, got %v", expected, actual) 529 } 530 } else if id == 2 { 531 w.Header().Set("Content-Type", "application/json") 532 w.WriteHeader(http.StatusOK) 533 fmt.Fprintln(w, `{"documentation":"https://bugzilla.redhat.com/docs/en/html/api/index.html","error":true,"code":32000,"message":"Subcomponet is mandatory for the component 'Cloud Compute' in the product 'OpenShift Container Platform'."}`) 534 } else { 535 http.Error(w, "404 Not Found", http.StatusNotFound) 536 } 537 } 538 })) 539 defer testServer.Close() 540 client := clientForUrl(testServer.URL) 541 542 update := BugUpdate{ 543 DependsOn: &IDUpdate{ 544 Add: []int{1705242}, 545 }, 546 Status: "UPDATED", 547 } 548 549 // this should run an update 550 if err := client.UpdateBug(1705243, update); err != nil { 551 t.Errorf("expected no error, but got one: %v", err) 552 } 553 554 // this should 404 555 err := client.UpdateBug(1, update) 556 if err == nil { 557 t.Error("expected an error, but got none") 558 } else if !IsNotFound(err) { 559 t.Errorf("expected a not found error, got %v", err) 560 } 561 562 // this is a 200 with an error payload 563 if err := client.UpdateBug(2, update); err == nil { 564 t.Error("expected an error, but got none") 565 } 566 } 567 568 func TestAddPullRequestAsExternalBug(t *testing.T) { 569 var testCases = []struct { 570 name string 571 trackerId uint 572 id int 573 expectedPayload string 574 response string 575 expectedError bool 576 expectedChanged bool 577 }{ 578 { 579 name: "update succeeds, makes a change", 580 id: 1705243, 581 expectedPayload: `{"jsonrpc":"1.0","method":"ExternalBugs.add_external_bug","params":[{"bug_ids":[1705243],"external_bugs":[{"ext_type_url":"https://github.com/","ext_bz_bug_id":"org/repo/pull/1"}]}],"id":"identifier"}`, 582 response: `{"error":null,"id":"identifier","result":{"bugs":[{"alias":[],"changes":{"ext_bz_bug_map.ext_bz_bug_id":{"added":"Github org/repo/pull/1","removed":""}},"id":1705243}]}}`, 583 expectedError: false, 584 expectedChanged: true, 585 }, 586 { 587 name: "explicit tracker ID is used, update succeeds, makes a change", 588 trackerId: 123, 589 id: 17052430, 590 expectedPayload: `{"jsonrpc":"1.0","method":"ExternalBugs.add_external_bug","params":[{"bug_ids":[17052430],"external_bugs":[{"ext_type_id":123,"ext_bz_bug_id":"org/repo/pull/1"}]}],"id":"identifier"}`, 591 response: `{"error":null,"id":"identifier","result":{"bugs":[{"alias":[],"changes":{"ext_bz_bug_map.ext_bz_bug_id":{"added":"Github org/repo/pull/1","removed":""}},"id":17052430}]}}`, 592 expectedError: false, 593 expectedChanged: true, 594 }, 595 { 596 name: "update succeeds, makes a change as part of multiple changes reported", 597 id: 1705244, 598 expectedPayload: `{"jsonrpc":"1.0","method":"ExternalBugs.add_external_bug","params":[{"bug_ids":[1705244],"external_bugs":[{"ext_type_url":"https://github.com/","ext_bz_bug_id":"org/repo/pull/1"}]}],"id":"identifier"}`, 599 response: `{"error":null,"id":"identifier","result":{"bugs":[{"alias":[],"changes":{"ext_bz_bug_map.ext_bz_bug_id":{"added":"Github org/repo/pull/1","removed":""}},"id":1705244},{"alias":[],"changes":{"ext_bz_bug_map.ext_bz_bug_id":{"added":"Github org/repo/pull/2","removed":""}},"id":1705244}]}}`, 600 expectedError: false, 601 expectedChanged: true, 602 }, 603 { 604 name: "update succeeds, makes no change", 605 id: 1705245, 606 expectedPayload: `{"jsonrpc":"1.0","method":"ExternalBugs.add_external_bug","params":[{"bug_ids":[1705245],"external_bugs":[{"ext_type_url":"https://github.com/","ext_bz_bug_id":"org/repo/pull/1"}]}],"id":"identifier"}`, 607 response: `{"error":null,"id":"identifier","result":{"bugs":[]}}`, 608 expectedError: false, 609 expectedChanged: false, 610 }, 611 { 612 name: "update fails, makes no change", 613 id: 1705246, 614 expectedPayload: `{"jsonrpc":"1.0","method":"ExternalBugs.add_external_bug","params":[{"bug_ids":[1705246],"external_bugs":[{"ext_type_url":"https://github.com/","ext_bz_bug_id":"org/repo/pull/1"}]}],"id":"identifier"}`, 615 response: `{"error":{"code": 100400,"message":"Invalid params for JSONRPC 1.0."},"id":"identifier","result":null}`, 616 expectedError: true, 617 expectedChanged: false, 618 }, 619 { 620 name: "get unrelated JSONRPC response", 621 id: 1705247, 622 expectedPayload: `{"jsonrpc":"1.0","method":"ExternalBugs.add_external_bug","params":[{"bug_ids":[1705247],"external_bugs":[{"ext_type_url":"https://github.com/","ext_bz_bug_id":"org/repo/pull/1"}]}],"id":"identifier"}`, 623 response: `{"error":null,"id":"oops","result":{"bugs":[]}}`, 624 expectedError: true, 625 expectedChanged: false, 626 }, 627 { 628 name: "update already made earlier, makes no change", 629 id: 1705248, 630 expectedPayload: `{"jsonrpc":"1.0","method":"ExternalBugs.add_external_bug","params":[{"bug_ids":[1705248],"external_bugs":[{"ext_type_url":"https://github.com/","ext_bz_bug_id":"org/repo/pull/1"}]}],"id":"identifier"}`, 631 response: `{"error":{"code": 100500,"message":"DBD::Pg::db do failed: ERROR: duplicate key value violates unique constraint \"ext_bz_bug_map_bug_id_idx\"\nDETAIL: Key (bug_id, ext_bz_id, ext_bz_bug_id)=(1778894, 131, openshift/installer/pull/2728) already exists. [for Statement \"INSERT INTO ext_bz_bug_map (ext_description, ext_bz_id, ext_bz_bug_id, ext_priority, ext_last_updated, bug_id, ext_status) VALUES (?,?,?,?,?,?,?)\"]\n\u003cpre\u003e\n at /var/www/html/bugzilla/Bugzilla/Object.pm line 754.\n\tBugzilla::Object::insert_create_data('Bugzilla::Extension::ExternalBugs::Bug', 'HASH(0x55eec2747a30)') called at /loader/0x55eec2720cc0/Bugzilla/Extension/ExternalBugs/Bug.pm line 118\n\tBugzilla::Extension::ExternalBugs::Bug::create('Bugzilla::Extension::ExternalBugs::Bug', 'HASH(0x55eed47b6d20)') called at /var/www/html/bugzilla/extensions/ExternalBugs/Extension.pm line 858\n\tBugzilla::Extension::ExternalBugs::bug_start_of_update('Bugzilla::Extension::ExternalBugs=HASH(0x55eecf484038)', 'HASH(0x55eed09302e8)') called at /var/www/html/bugzilla/Bugzilla/Hook.pm line 21\n\tBugzilla::Hook::process('bug_start_of_update', 'HASH(0x55eed09302e8)') called at /var/www/html/bugzilla/Bugzilla/Bug.pm line 1168\n\tBugzilla::Bug::update('Bugzilla::Bug=HASH(0x55eed048b350)') called at /loader/0x55eec2720cc0/Bugzilla/Extension/ExternalBugs/WebService.pm line 80\n\tBugzilla::Extension::ExternalBugs::WebService::add_external_bug('Bugzilla::WebService::Server::JSONRPC::Bugzilla::Extension::E...', 'HASH(0x55eed38bd710)') called at (eval 5435) line 1\n\teval ' $procedure-\u003e{code}-\u003e($self, @params) \n;' called at /usr/share/perl5/vendor_perl/JSON/RPC/Legacy/Server.pm line 220\n\tJSON::RPC::Legacy::Server::_handle('Bugzilla::WebService::Server::JSONRPC::Bugzilla::Extension::E...', 'HASH(0x55eed1990ef0)') called at /var/www/html/bugzilla/Bugzilla/WebService/Server/JSONRPC.pm line 295\n\tBugzilla::WebService::Server::JSONRPC::_handle('Bugzilla::WebService::Server::JSONRPC::Bugzilla::Extension::E...', 'HASH(0x55eed1990ef0)') called at /usr/share/perl5/vendor_perl/JSON/RPC/Legacy/Server.pm line 126\n\tJSON::RPC::Legacy::Server::handle('Bugzilla::WebService::Server::JSONRPC::Bugzilla::Extension::E...') called at /var/www/html/bugzilla/Bugzilla/WebService/Server/JSONRPC.pm line 70\n\tBugzilla::WebService::Server::JSONRPC::handle('Bugzilla::WebService::Server::JSONRPC::Bugzilla::Extension::E...') called at /var/www/html/bugzilla/jsonrpc.cgi line 31\n\tModPerl::ROOT::Bugzilla::ModPerl::ResponseHandler::var_www_html_bugzilla_jsonrpc_2ecgi::handler('Apache2::RequestRec=SCALAR(0x55eed3231870)') called at /usr/lib64/perl5/vendor_perl/ModPerl/RegistryCooker.pm line 207\n\teval {...} called at /usr/lib64/perl5/vendor_perl/ModPerl/RegistryCooker.pm line 207\n\tModPerl::RegistryCooker::run('Bugzilla::ModPerl::ResponseHandler=HASH(0x55eed023da08)') called at /usr/lib64/perl5/vendor_perl/ModPerl/RegistryCooker.pm line 173\n\tModPerl::RegistryCooker::default_handler('Bugzilla::ModPerl::ResponseHandler=HASH(0x55eed023da08)') called at /usr/lib64/perl5/vendor_perl/ModPerl/Registry.pm line 32\n\tModPerl::Registry::handler('Bugzilla::ModPerl::ResponseHandler', 'Apache2::RequestRec=SCALAR(0x55eed3231870)') called at /var/www/html/bugzilla/mod_perl.pl line 139\n\tBugzilla::ModPerl::ResponseHandler::handler('Bugzilla::ModPerl::ResponseHandler', 'Apache2::RequestRec=SCALAR(0x55eed3231870)') called at (eval 5435) line 0\n\teval {...} called at (eval 5435) line 0\n\n\u003c/pre\u003e at /var/www/html/bugzilla/Bugzilla/Object.pm line 754.\n at /var/www/html/bugzilla/Bugzilla/Object.pm line 754.\n\tBugzilla::Object::insert_create_data('Bugzilla::Extension::ExternalBugs::Bug', 'HASH(0x55eec2747a30)') called at /loader/0x55eec2720cc0/Bugzilla/Extension/ExternalBugs/Bug.pm line 118\n\tBugzilla::Extension::ExternalBugs::Bug::create('Bugzilla::Extension::ExternalBugs::Bug', 'HASH(0x55eed47b6d20)') called at /var/www/html/bugzilla/extensions/ExternalBugs/Extension.pm line 858\n\tBugzilla::Extension::ExternalBugs::bug_start_of_update('Bugzilla::Extension::ExternalBugs=HASH(0x55eecf484038)', 'HASH(0x55eed09302e8)') called at /var/www/html/bugzilla/Bugzilla/Hook.pm line 21\n\tBugzilla::Hook::process('bug_start_of_update', 'HASH(0x55eed09302e8)') called at /var/www/html/bugzilla/Bugzilla/Bug.pm line 1168\n\tBugzilla::Bug::update('Bugzilla::Bug=HASH(0x55eed048b350)') called at /loader/0x55eec2720cc0/Bugzilla/Extension/ExternalBugs/WebService.pm line 80\n\tBugzilla::Extension::ExternalBugs::WebService::add_external_bug('Bugzilla::WebService::Server::JSONRPC::Bugzilla::Extension::E...', 'HASH(0x55eed38bd710)') called at (eval 5435) line 1\n\teval ' $procedure-\u003e{code}-\u003e($self, @params) \n;' called at /usr/share/perl5/vendor_perl/JSON/RPC/Legacy/Server.pm line 220\n\tJSON::RPC::Legacy::Server::_handle('Bugzilla::WebService::Server::JSONRPC::Bugzilla::Extension::E...', 'HASH(0x55eed1990ef0)') called at /var/www/html/bugzilla/Bugzilla/WebService/Server/JSONRPC.pm line 295\n\tBugzilla::WebService::Server::JSONRPC::_handle('Bugzilla::WebService::Server::JSONRPC::Bugzilla::Extension::E...', 'HASH(0x55eed1990ef0)') called at /usr/share/perl5/vendor_perl/JSON/RPC/Legacy/Server.pm line 126\n\tJSON::RPC::Legacy::Server::handle('Bugzilla::WebService::Server::JSONRPC::Bugzilla::Extension::E...') called at /var/www/html/bugzilla/Bugzilla/WebService/Server/JSONRPC.pm line 70\n\tBugzilla::WebService::Server::JSONRPC::handle('Bugzilla::WebService::Server::JSONRPC::Bugzilla::Extension::E...') called at /var/www/html/bugzilla/jsonrpc.cgi line 31\n\tModPerl::ROOT::Bugzilla::ModPerl::ResponseHandler::var_www_html_bugzilla_jsonrpc_2ecgi::handler('Apache2::RequestRec=SCALAR(0x55eed3231870)') called at /usr/lib64/perl5/vendor_perl/ModPerl/RegistryCooker.pm line 207\n\teval {...} called at /usr/lib64/perl5/vendor_perl/ModPerl/RegistryCooker.pm line 207\n\tModPerl::RegistryCooker::run('Bugzilla::ModPerl::ResponseHandler=HASH(0x55eed023da08)') called at /usr/lib64/perl5/vendor_perl/ModPerl/RegistryCooker.pm line 173\n\tModPerl::RegistryCooker::default_handler('Bugzilla::ModPerl::ResponseHandler=HASH(0x55eed023da08)') called at /usr/lib64/perl5/vendor_perl/ModPerl/Registry.pm line 32\n\tModPerl::Registry::handler('Bugzilla::ModPerl::ResponseHandler', 'Apache2::RequestRec=SCALAR(0x55eed3231870)') called at /var/www/html/bugzilla/mod_perl.pl line 139\n\tBugzilla::ModPerl::ResponseHandler::handler('Bugzilla::ModPerl::ResponseHandler', 'Apache2::RequestRec=SCALAR(0x55eed3231870)') called at (eval 5435) line 0\n\teval {...} called at (eval 5435) line 0"},"id":"identifier","result":null}`, 632 expectedError: false, 633 expectedChanged: false, 634 }, 635 } 636 testServer := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 637 if r.Header.Get("Content-Type") != "application/json" { 638 t.Error("did not correctly set content-type header for JSON") 639 http.Error(w, "403 Forbidden", http.StatusForbidden) 640 return 641 } 642 if r.Method != http.MethodPost { 643 t.Errorf("incorrect method to use the JSONRPC API: %s", r.Method) 644 http.Error(w, "400 Bad Request", http.StatusBadRequest) 645 return 646 } 647 if r.URL.Path != "/jsonrpc.cgi" { 648 t.Errorf("incorrect path to use the JSONRPC API: %s", r.URL.Path) 649 http.Error(w, "400 Bad Request", http.StatusBadRequest) 650 return 651 } 652 var payload struct { 653 // Version is the version of JSONRPC to use. All Bugzilla servers 654 // support 1.0. Some support 1.1 and some support 2.0 655 Version string `json:"jsonrpc"` 656 Method string `json:"method"` 657 // Parameters must be specified in JSONRPC 1.0 as a structure in the first 658 // index of this slice 659 Parameters []AddExternalBugParameters `json:"params"` 660 ID string `json:"id"` 661 } 662 raw, err := io.ReadAll(r.Body) 663 if err != nil { 664 t.Errorf("failed to read request body: %v", err) 665 http.Error(w, "500 Server Error", http.StatusInternalServerError) 666 return 667 } 668 if err := json.Unmarshal(raw, &payload); err != nil { 669 t.Errorf("malformed JSONRPC payload: %s", string(raw)) 670 http.Error(w, "400 Bad Request", http.StatusBadRequest) 671 return 672 } 673 for _, testCase := range testCases { 674 if payload.Parameters[0].BugIDs[0] == testCase.id { 675 if diff := cmp.Diff(string(raw), testCase.expectedPayload); diff != "" { 676 t.Errorf("%s: got incorrect JSONRPC payload: %v", testCase.name, diff) 677 } 678 if _, err := w.Write([]byte(testCase.response)); err != nil { 679 t.Fatalf("%s: failed to send JSONRPC response: %v", testCase.name, err) 680 } 681 return 682 } 683 } 684 http.Error(w, "404 Not Found", http.StatusNotFound) 685 })) 686 defer testServer.Close() 687 client := clientForUrl(testServer.URL) 688 689 for _, testCase := range testCases { 690 t.Run(testCase.name, func(t *testing.T) { 691 client.githubExternalTrackerId = testCase.trackerId 692 changed, err := client.AddPullRequestAsExternalBug(testCase.id, "org", "repo", 1) 693 if !testCase.expectedError && err != nil { 694 t.Errorf("%s: expected no error, but got one: %v", testCase.name, err) 695 } 696 if testCase.expectedError && err == nil { 697 t.Errorf("%s: expected an error, but got none", testCase.name) 698 } 699 if testCase.expectedChanged != changed { 700 t.Errorf("%s: got incorrect state change", testCase.name) 701 } 702 }) 703 } 704 705 // this should 404 706 changed, err := client.AddPullRequestAsExternalBug(1, "org", "repo", 1) 707 if err == nil { 708 t.Error("expected an error, but got none") 709 } else if !IsNotFound(err) { 710 t.Errorf("expected a not found error, got %v", err) 711 } 712 if changed { 713 t.Error("expected not to change state, but did") 714 } 715 } 716 717 func TestRemovePullRequestAsExternalBug(t *testing.T) { 718 var testCases = []struct { 719 name string 720 id int 721 expectedPayload string 722 response string 723 expectedError bool 724 expectedChanged bool 725 }{ 726 { 727 name: "update succeeds, makes a change", 728 id: 1705243, 729 expectedPayload: `{"jsonrpc":"1.0","method":"ExternalBugs.remove_external_bug","params":[{"bug_ids":[1705243],"ext_type_url":"https://github.com/","ext_bz_bug_id":"org/repo/pull/1"}],"id":"identifier"}`, 730 response: `{"error":null,"id":"identifier","result":{"external_bugs":[{"ext_type_url":"https://github.com/","ext_bz_bug_id":"org/repo/pull/1"}]}}`, 731 expectedError: false, 732 expectedChanged: true, 733 }, 734 { 735 name: "update succeeds, makes a change as part of multiple changes reported", 736 id: 1705244, 737 expectedPayload: `{"jsonrpc":"1.0","method":"ExternalBugs.remove_external_bug","params":[{"bug_ids":[1705244],"ext_type_url":"https://github.com/","ext_bz_bug_id":"org/repo/pull/1"}],"id":"identifier"}`, 738 response: `{"error":null,"id":"identifier","result":{"external_bugs":[{"ext_type_url":"https://github.com/","ext_bz_bug_id":"org/repo/pull/1"},{"ext_type_url":"https://github.com/","ext_bz_bug_id":"org/repo/pull/2"}]}}`, 739 expectedError: false, 740 expectedChanged: true, 741 }, 742 { 743 name: "update succeeds, makes no change", 744 id: 1705245, 745 expectedPayload: `{"jsonrpc":"1.0","method":"ExternalBugs.remove_external_bug","params":[{"bug_ids":[1705245],"ext_type_url":"https://github.com/","ext_bz_bug_id":"org/repo/pull/1"}],"id":"identifier"}`, 746 response: `{"error":null,"id":"identifier","result":{"external_bugs":[]}}`, 747 expectedError: false, 748 expectedChanged: false, 749 }, 750 { 751 name: "update fails, makes no change", 752 id: 1705246, 753 expectedPayload: `{"jsonrpc":"1.0","method":"ExternalBugs.remove_external_bug","params":[{"bug_ids":[1705246],"ext_type_url":"https://github.com/","ext_bz_bug_id":"org/repo/pull/1"}],"id":"identifier"}`, 754 response: `{"error":{"code": 100400,"message":"Invalid params for JSONRPC 1.0."},"id":"identifier","result":null}`, 755 expectedError: true, 756 expectedChanged: false, 757 }, 758 { 759 name: "get unrelated JSONRPC response", 760 id: 1705247, 761 expectedPayload: `{"jsonrpc":"1.0","method":"ExternalBugs.remove_external_bug","params":[{"bug_ids":[1705247],"ext_type_url":"https://github.com/","ext_bz_bug_id":"org/repo/pull/1"}],"id":"identifier"}`, 762 response: `{"error":null,"id":"oops","result":{"external_bugs":[]}}`, 763 expectedError: true, 764 expectedChanged: false, 765 }, 766 } 767 testServer := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 768 if r.Header.Get("Content-Type") != "application/json" { 769 t.Error("did not correctly set content-type header for JSON") 770 http.Error(w, "403 Forbidden", http.StatusForbidden) 771 return 772 } 773 if r.Method != http.MethodPost { 774 t.Errorf("incorrect method to use the JSONRPC API: %s", r.Method) 775 http.Error(w, "400 Bad Request", http.StatusBadRequest) 776 return 777 } 778 if r.URL.Path != "/jsonrpc.cgi" { 779 t.Errorf("incorrect path to use the JSONRPC API: %s", r.URL.Path) 780 http.Error(w, "400 Bad Request", http.StatusBadRequest) 781 return 782 } 783 var payload struct { 784 // Version is the version of JSONRPC to use. All Bugzilla servers 785 // support 1.0. Some support 1.1 and some support 2.0 786 Version string `json:"jsonrpc"` 787 Method string `json:"method"` 788 // Parameters must be specified in JSONRPC 1.0 as a structure in the first 789 // index of this slice 790 Parameters []RemoveExternalBugParameters `json:"params"` 791 ID string `json:"id"` 792 } 793 raw, err := io.ReadAll(r.Body) 794 if err != nil { 795 t.Errorf("failed to read request body: %v", err) 796 http.Error(w, "500 Server Error", http.StatusInternalServerError) 797 return 798 } 799 if err := json.Unmarshal(raw, &payload); err != nil { 800 t.Errorf("malformed JSONRPC payload: %s", string(raw)) 801 http.Error(w, "400 Bad Request", http.StatusBadRequest) 802 return 803 } 804 for _, testCase := range testCases { 805 if payload.Parameters[0].BugIDs[0] == testCase.id { 806 if actual, expected := string(raw), testCase.expectedPayload; actual != expected { 807 t.Errorf("%s: got incorrect JSONRPC payload: %v", testCase.name, diff.ObjectReflectDiff(expected, actual)) 808 } 809 if _, err := w.Write([]byte(testCase.response)); err != nil { 810 t.Fatalf("%s: failed to send JSONRPC response: %v", testCase.name, err) 811 } 812 return 813 } 814 } 815 http.Error(w, "404 Not Found", http.StatusNotFound) 816 })) 817 defer testServer.Close() 818 client := clientForUrl(testServer.URL) 819 820 for _, testCase := range testCases { 821 t.Run(testCase.name, func(t *testing.T) { 822 changed, err := client.RemovePullRequestAsExternalBug(testCase.id, "org", "repo", 1) 823 if !testCase.expectedError && err != nil { 824 t.Errorf("%s: expected no error, but got one: %v", testCase.name, err) 825 } 826 if testCase.expectedError && err == nil { 827 t.Errorf("%s: expected an error, but got none", testCase.name) 828 } 829 if testCase.expectedChanged != changed { 830 t.Errorf("%s: got incorrect state change", testCase.name) 831 } 832 }) 833 } 834 835 // this should 404 836 changed, err := client.AddPullRequestAsExternalBug(1, "org", "repo", 1) 837 if err == nil { 838 t.Error("expected an error, but got none") 839 } else if !IsNotFound(err) { 840 t.Errorf("expected a not found error, got %v", err) 841 } 842 if changed { 843 t.Error("expected not to change state, but did") 844 } 845 } 846 847 func TestIdentifierForPull(t *testing.T) { 848 var testCases = []struct { 849 name string 850 org, repo string 851 num int 852 expected string 853 }{ 854 { 855 name: "normal works as expected", 856 org: "organization", 857 repo: "repository", 858 num: 1234, 859 expected: "organization/repository/pull/1234", 860 }, 861 } 862 863 for _, testCase := range testCases { 864 if actual, expected := IdentifierForPull(testCase.org, testCase.repo, testCase.num), testCase.expected; actual != expected { 865 t.Errorf("%s: got incorrect identifier, expected %s but got %s", testCase.name, expected, actual) 866 } 867 } 868 } 869 870 func TestPullFromIdentifier(t *testing.T) { 871 var testCases = []struct { 872 name string 873 identifier string 874 expectedOrg, expectedRepo string 875 expectedNum int 876 expectedErr bool 877 expectedNotPullErr bool 878 }{ 879 { 880 name: "normal works as expected", 881 identifier: "organization/repository/pull/1234", 882 expectedOrg: "organization", 883 expectedRepo: "repository", 884 expectedNum: 1234, 885 }, 886 { 887 name: "extra `/` at end works correctly", 888 identifier: "organization/repository/pull/1234/", 889 expectedOrg: "organization", 890 expectedRepo: "repository", 891 expectedNum: 1234, 892 }, 893 { 894 name: "extra `/files` included works correctly", 895 identifier: "organization/repository/pull/1234/files", 896 expectedOrg: "organization", 897 expectedRepo: "repository", 898 expectedNum: 1234, 899 }, 900 { 901 name: "extra `/files/` included works correctly", 902 identifier: "organization/repository/pull/1234/files/", 903 expectedOrg: "organization", 904 expectedRepo: "repository", 905 expectedNum: 1234, 906 }, 907 { 908 name: "wrong number of parts fails", 909 identifier: "organization/repository", 910 expectedErr: true, 911 }, 912 { 913 name: "not a pull fails but in an identifiable way", 914 identifier: "organization/repository/issue/1234", 915 expectedErr: true, 916 expectedNotPullErr: true, 917 }, 918 { 919 name: "not a number fails", 920 identifier: "organization/repository/pull/abcd", 921 expectedErr: true, 922 }, 923 } 924 925 for _, testCase := range testCases { 926 org, repo, num, err := PullFromIdentifier(testCase.identifier) 927 if testCase.expectedErr && err == nil { 928 t.Errorf("%s: expected an error but got none", testCase.name) 929 } 930 if !testCase.expectedErr && err != nil { 931 t.Errorf("%s: expected no error but got one: %v", testCase.name, err) 932 } 933 if testCase.expectedNotPullErr && !IsIdentifierNotForPullErr(err) { 934 t.Errorf("%s: expected a notForPull error but got: %T", testCase.name, err) 935 } 936 if org != testCase.expectedOrg { 937 t.Errorf("%s: got incorrect org, expected %s but got %s", testCase.name, testCase.expectedOrg, org) 938 } 939 if repo != testCase.expectedRepo { 940 t.Errorf("%s: got incorrect repo, expected %s but got %s", testCase.name, testCase.expectedRepo, repo) 941 } 942 if num != testCase.expectedNum { 943 t.Errorf("%s: got incorrect num, expected %d but got %d", testCase.name, testCase.expectedNum, num) 944 } 945 } 946 } 947 948 func TestGetExternalBugPRsOnBug(t *testing.T) { 949 var testCases = []struct { 950 name string 951 id int 952 response string 953 expectedError bool 954 expectedPRs []ExternalBug 955 }{ 956 { 957 name: "no external bugs returns empty list", 958 id: 1705243, 959 response: `{"bugs":[{"external_bugs":[]}],"faults":[]}`, 960 expectedError: false, 961 }, 962 { 963 name: "one external bug pointing to PR is found", 964 id: 1705244, 965 response: `{"bugs":[{"external_bugs":[{"bug_id": 1705244,"ext_bz_bug_id":"org/repo/pull/1","type":{"url":"https://github.com/"}}]}],"faults":[]}`, 966 expectedError: false, 967 expectedPRs: []ExternalBug{{Type: ExternalBugType{URL: "https://github.com/"}, BugzillaBugID: 1705244, ExternalBugID: "org/repo/pull/1", Org: "org", Repo: "repo", Num: 1}}, 968 }, 969 { 970 name: "multiple external bugs pointing to PRs are found", 971 id: 1705245, 972 response: `{"bugs":[{"external_bugs":[{"bug_id": 1705245,"ext_bz_bug_id":"org/repo/pull/1","type":{"url":"https://github.com/"}},{"bug_id": 1705245,"ext_bz_bug_id":"org/repo/pull/2","type":{"url":"https://github.com/"}}]}],"faults":[]}`, 973 expectedError: false, 974 expectedPRs: []ExternalBug{{Type: ExternalBugType{URL: "https://github.com/"}, BugzillaBugID: 1705245, ExternalBugID: "org/repo/pull/1", Org: "org", Repo: "repo", Num: 1}, {Type: ExternalBugType{URL: "https://github.com/"}, BugzillaBugID: 1705245, ExternalBugID: "org/repo/pull/2", Org: "org", Repo: "repo", Num: 2}}, 975 }, 976 { 977 name: "external bugs pointing to issues are ignored", 978 id: 1705246, 979 response: `{"bugs":[{"external_bugs":[{"bug_id": 1705246,"ext_bz_bug_id":"org/repo/issues/1","type":{"url":"https://github.com/"}}]}],"faults":[]}`, 980 expectedError: false, 981 }, 982 { 983 name: "external bugs pointing to other Bugzilla bugs are ignored", 984 id: 1705247, 985 response: `{"bugs":[{"external_bugs":[{"bug_id": 3,"ext_bz_bug_id":"org/repo/pull/1","type":{"url":"https://github.com/"}}]}],"faults":[]}`, 986 expectedError: false, 987 }, 988 { 989 name: "external bugs pointing to other trackers are ignored", 990 id: 1705248, 991 response: `{"bugs":[{"external_bugs":[{"bug_id": 1705248,"ext_bz_bug_id":"something","type":{"url":"https://bugs.tracker.com/"}}]}],"faults":[]}`, 992 expectedError: false, 993 }, 994 { 995 name: "external bugs pointing to invalid pulls cause an error", 996 id: 1705249, 997 response: `{"bugs":[{"external_bugs":[{"bug_id": 1705249,"ext_bz_bug_id":"org/repo/pull/c","type":{"url":"https://github.com/"}}]}],"faults":[]}`, 998 expectedError: true, 999 }, 1000 } 1001 testServer := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 1002 if r.Header.Get("X-BUGZILLA-API-KEY") != "api-key" { 1003 t.Error("did not get api-key passed in X-BUGZILLA-API-KEY header") 1004 http.Error(w, "403 Forbidden", http.StatusForbidden) 1005 return 1006 } 1007 if r.URL.Query().Get("include_fields") != "external_bugs" { 1008 t.Error("did not get external bugs passed in include_fields query parameter") 1009 http.Error(w, "400 Bad Request", http.StatusBadRequest) 1010 return 1011 } 1012 if r.Method != http.MethodGet { 1013 t.Errorf("incorrect method to get a bug: %s", r.Method) 1014 http.Error(w, "400 Bad Request", http.StatusBadRequest) 1015 return 1016 } 1017 if !strings.HasPrefix(r.URL.Path, "/rest/bug/") { 1018 t.Errorf("incorrect path to get a bug: %s", r.URL.Path) 1019 http.Error(w, "400 Bad Request", http.StatusBadRequest) 1020 return 1021 } 1022 id, err := strconv.Atoi(strings.TrimPrefix(r.URL.Path, "/rest/bug/")) 1023 if err != nil { 1024 t.Errorf("malformed bug id: %s", r.URL.Path) 1025 http.Error(w, "400 Bad Request", http.StatusBadRequest) 1026 return 1027 } 1028 for _, testCase := range testCases { 1029 if id == testCase.id { 1030 if _, err := w.Write([]byte(testCase.response)); err != nil { 1031 t.Fatalf("%s: failed to send response: %v", testCase.name, err) 1032 } 1033 return 1034 } 1035 } 1036 1037 })) 1038 defer testServer.Close() 1039 client := clientForUrl(testServer.URL) 1040 1041 for _, testCase := range testCases { 1042 t.Run(testCase.name, func(t *testing.T) { 1043 prs, err := client.GetExternalBugPRsOnBug(testCase.id) 1044 if !testCase.expectedError && err != nil { 1045 t.Errorf("%s: expected no error, but got one: %v", testCase.name, err) 1046 } 1047 if testCase.expectedError && err == nil { 1048 t.Errorf("%s: expected an error, but got none", testCase.name) 1049 } 1050 if diff := cmp.Diff(prs, testCase.expectedPRs); diff != "" { 1051 t.Errorf("%s: got incorrect prs: %v", testCase.name, diff) 1052 } 1053 }) 1054 } 1055 } 1056 func errorChecker(err error, t *testing.T) { 1057 if err != nil { 1058 t.Fatalf("Error while creating bugs for testing while calling the mocked endpoint!!") 1059 } 1060 } 1061 func TestGetAllClones(t *testing.T) { 1062 1063 testcases := []struct { 1064 name string 1065 bugs []Bug 1066 bugToBeSearched Bug 1067 expectedClones sets.Set[int] 1068 }{ 1069 { 1070 name: "Clones for the root node", 1071 bugs: []Bug{ 1072 {Summary: "", ID: 1, Blocks: []int{2, 5}}, 1073 {Summary: "", ID: 2, DependsOn: []int{1}, Blocks: []int{3}}, 1074 {Summary: "", ID: 3, DependsOn: []int{2}}, 1075 {Summary: "Not a clone", ID: 4, DependsOn: []int{1}}, 1076 {Summary: "", ID: 5, DependsOn: []int{1}}, 1077 }, 1078 bugToBeSearched: Bug{Summary: "", ID: 1, Blocks: []int{2, 5}}, 1079 expectedClones: sets.New[int](1, 2, 3, 5), 1080 }, 1081 { 1082 name: "Clones for child of root", 1083 bugs: []Bug{ 1084 {Summary: "", ID: 1, Blocks: []int{2, 5}}, 1085 {Summary: "", ID: 2, DependsOn: []int{1}, Blocks: []int{3}}, 1086 {Summary: "", ID: 3, DependsOn: []int{2}}, 1087 {Summary: "Not a clone", ID: 4, DependsOn: []int{1}}, 1088 {Summary: "", ID: 5, DependsOn: []int{1}}, 1089 }, 1090 bugToBeSearched: Bug{Summary: "", ID: 2, DependsOn: []int{1}, Blocks: []int{3}}, 1091 expectedClones: sets.New[int](1, 2, 3, 5), 1092 }, 1093 { 1094 name: "Clones for grandchild of root", 1095 bugs: []Bug{ 1096 {Summary: "", ID: 1, Blocks: []int{2, 5}}, 1097 {Summary: "", ID: 2, DependsOn: []int{1}, Blocks: []int{3}}, 1098 {Summary: "", ID: 3, DependsOn: []int{2}}, 1099 {Summary: "Not a clone", ID: 4, DependsOn: []int{1}}, 1100 {Summary: "", ID: 5, DependsOn: []int{1}}, 1101 }, 1102 bugToBeSearched: Bug{Summary: "", ID: 3, DependsOn: []int{2}}, 1103 expectedClones: sets.New[int](1, 2, 3, 5), 1104 }, 1105 { 1106 name: "Clones when no clone is expected", 1107 bugs: []Bug{ 1108 {Summary: "", ID: 1, Blocks: []int{2, 5}}, 1109 {Summary: "", ID: 2, DependsOn: []int{1}, Blocks: []int{3}}, 1110 {Summary: "", ID: 3, DependsOn: []int{2}}, 1111 {Summary: "Not a clone", ID: 4, DependsOn: []int{1}}, 1112 {Summary: "", ID: 5, DependsOn: []int{1}}, 1113 }, 1114 bugToBeSearched: Bug{Summary: "Not a clone", ID: 4, DependsOn: []int{1}}, 1115 expectedClones: sets.New[int](4), 1116 }, 1117 } 1118 for _, tc := range testcases { 1119 t.Run(tc.name, func(t *testing.T) { 1120 fake := &Fake{ 1121 Bugs: map[int]Bug{}, 1122 BugComments: map[int][]Comment{}, 1123 } 1124 for _, bug := range tc.bugs { 1125 fake.Bugs[bug.ID] = bug 1126 } 1127 bugCache := newBugDetailsCache() 1128 clones, err := getAllClones(fake, &tc.bugToBeSearched, bugCache) 1129 if err != nil { 1130 t.Errorf("Error occurred when none was expected: %v", err) 1131 } 1132 actualCloneSet := sets.New[int]() 1133 for _, clone := range clones { 1134 actualCloneSet.Insert(clone.ID) 1135 } 1136 if !tc.expectedClones.Equal(actualCloneSet) { 1137 t.Errorf("clones mismatch - expected %v, got %v", tc.expectedClones, actualCloneSet) 1138 } 1139 1140 }) 1141 1142 } 1143 1144 } 1145 1146 func TestGetRootForClone(t *testing.T) { 1147 fake := &Fake{} 1148 fake.Bugs = map[int]Bug{} 1149 fake.BugComments = map[int][]Comment{} 1150 bug1Create := &BugCreate{ 1151 Summary: "Dummy bug to test getAllClones", 1152 } 1153 bugDiffCreate := &BugCreate{ 1154 Summary: "Different bug", 1155 } 1156 diffBugID, err := fake.CreateBug(bugDiffCreate) 1157 errorChecker(err, t) 1158 bug1ID, err := fake.CreateBug(bug1Create) 1159 if err != nil { 1160 t.Fatalf("Error while creating bug in Fake!\n") 1161 } 1162 idUpdate := &IDUpdate{ 1163 Add: []int{diffBugID}, 1164 } 1165 update := BugUpdate{ 1166 DependsOn: idUpdate, 1167 } 1168 fake.UpdateBug(bug1ID, update) 1169 bug1, err := fake.GetBug(bug1ID) 1170 errorChecker(err, t) 1171 bug2ID, err := fake.CloneBug(bug1) 1172 errorChecker(err, t) 1173 bug2, err := fake.GetBug(bug2ID) 1174 errorChecker(err, t) 1175 bug3ID, err := fake.CloneBug(bug2) 1176 errorChecker(err, t) 1177 bug1, err = fake.GetBug(bug1ID) 1178 errorChecker(err, t) 1179 bug2, err = fake.GetBug(bug2ID) 1180 errorChecker(err, t) 1181 bug3, err := fake.GetBug(bug3ID) 1182 errorChecker(err, t) 1183 testcases := []struct { 1184 name string 1185 bugPtr *Bug 1186 expectedRoot int 1187 }{ 1188 { 1189 "Root is itself", 1190 bug1, 1191 bug1ID, 1192 }, 1193 { 1194 "Root is immediate parent", 1195 bug2, 1196 bug1ID, 1197 }, 1198 { 1199 "Root is grandparent", 1200 bug3, 1201 bug1ID, 1202 }, 1203 } 1204 for _, tc := range testcases { 1205 t.Run(tc.name, func(t *testing.T) { 1206 // this should run get the root 1207 root, err := getRootForClone(fake, tc.bugPtr) 1208 if err != nil { 1209 t.Errorf("Error occurred when error not expected: %v", err) 1210 } 1211 if root.ID != tc.expectedRoot { 1212 t.Errorf("ID of root incorrect.") 1213 } 1214 }) 1215 } 1216 } 1217 1218 func TestClone(t *testing.T) { 1219 t.Parallel() 1220 testCases := []struct { 1221 name string 1222 mutations []func(bug *BugCreate) 1223 original *Bug 1224 expected Bug 1225 }{ 1226 { 1227 name: "Simple", 1228 original: &Bug{ID: 1}, 1229 expected: Bug{DependsOn: []int{1}}, 1230 }, 1231 { 1232 name: "Simple with mutation", 1233 mutations: []func(bug *BugCreate){ 1234 func(bug *BugCreate) { 1235 bug.TargetRelease = []string{"1.2"} 1236 }, 1237 }, 1238 original: &Bug{ID: 1}, 1239 expected: Bug{ 1240 DependsOn: []int{1}, 1241 TargetRelease: []string{"1.2"}, 1242 }, 1243 }, 1244 { 1245 name: "Simple with multiple mutations", 1246 mutations: []func(bug *BugCreate){ 1247 func(bug *BugCreate) { 1248 bug.TargetRelease = []string{"1.2"} 1249 }, 1250 func(bug *BugCreate) { 1251 bug.CC = []string{"test@test.com", "foo@bar.com"} 1252 }, 1253 }, 1254 original: &Bug{ID: 1}, 1255 expected: Bug{ 1256 DependsOn: []int{1}, 1257 TargetRelease: []string{"1.2"}, 1258 CC: []string{"test@test.com", "foo@bar.com"}, 1259 }, 1260 }, 1261 { 1262 name: "Copy blocks field", 1263 original: &Bug{ID: 1, Blocks: []int{0}}, 1264 expected: Bug{DependsOn: []int{1}, Blocks: []int{0}}, 1265 }, 1266 } 1267 1268 for _, tc := range testCases { 1269 t.Run(tc.name, func(t *testing.T) { 1270 client := &Fake{Bugs: map[int]Bug{0: {}}, BugComments: map[int][]Comment{1: {{}}}} 1271 newID, err := clone(client, tc.original, tc.mutations) 1272 if err != nil { 1273 t.Fatalf("cloning failed: %v", err) 1274 } 1275 tc.expected.ID = newID 1276 if diff := cmp.Diff(tc.expected, client.Bugs[newID]); diff != "" { 1277 t.Errorf("expected clone differs from actual clone: %s", diff) 1278 } 1279 }) 1280 } 1281 }