sigs.k8s.io/prow@v0.0.0-20240503223140-c5e374dc7eb1/pkg/plugins/config_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 plugins 18 19 import ( 20 "errors" 21 "fmt" 22 "reflect" 23 "strings" 24 "testing" 25 "time" 26 27 "github.com/google/go-cmp/cmp" 28 fuzz "github.com/google/gofuzz" 29 30 apiequality "k8s.io/apimachinery/pkg/api/equality" 31 "k8s.io/apimachinery/pkg/util/diff" 32 "k8s.io/apimachinery/pkg/util/sets" 33 utilpointer "k8s.io/utils/pointer" 34 "sigs.k8s.io/yaml" 35 36 "sigs.k8s.io/prow/pkg/bugzilla" 37 "sigs.k8s.io/prow/pkg/plugins/ownersconfig" 38 ) 39 40 func TestValidateExternalPlugins(t *testing.T) { 41 tests := []struct { 42 name string 43 plugins map[string][]ExternalPlugin 44 expectedErr error 45 }{ 46 { 47 name: "valid config", 48 plugins: map[string][]ExternalPlugin{ 49 "kubernetes/test-infra": { 50 { 51 Name: "cherrypick", 52 }, 53 { 54 Name: "configupdater", 55 }, 56 { 57 Name: "tetris", 58 }, 59 }, 60 "kubernetes": { 61 { 62 Name: "coffeemachine", 63 }, 64 { 65 Name: "blender", 66 }, 67 }, 68 }, 69 expectedErr: nil, 70 }, 71 { 72 name: "invalid config", 73 plugins: map[string][]ExternalPlugin{ 74 "kubernetes/test-infra": { 75 { 76 Name: "cherrypick", 77 }, 78 { 79 Name: "configupdater", 80 }, 81 { 82 Name: "tetris", 83 }, 84 }, 85 "kubernetes": { 86 { 87 Name: "coffeemachine", 88 }, 89 { 90 Name: "tetris", 91 }, 92 }, 93 }, 94 expectedErr: errors.New("invalid plugin configuration:\n\texternal plugins [tetris] are duplicated for kubernetes/test-infra and kubernetes"), 95 }, 96 } 97 98 for _, test := range tests { 99 t.Logf("Running scenario %q", test.name) 100 101 err := validateExternalPlugins(test.plugins) 102 if !reflect.DeepEqual(err, test.expectedErr) { 103 t.Errorf("unexpected error: %v, expected: %v", err, test.expectedErr) 104 } 105 } 106 } 107 108 func TestOwnersFilenames(t *testing.T) { 109 cases := []struct { 110 org string 111 repo string 112 config Owners 113 expected ownersconfig.Filenames 114 }{ 115 { 116 org: "kubernetes", 117 repo: "test-infra", 118 config: Owners{ 119 Filenames: map[string]ownersconfig.Filenames{ 120 "kubernetes": {Owners: "OWNERS", OwnersAliases: "OWNERS_ALIASES"}, 121 "kubernetes/test-infra": {Owners: ".OWNERS", OwnersAliases: ".OWNERS_ALIASES"}, 122 }, 123 }, 124 expected: ownersconfig.Filenames{ 125 Owners: ".OWNERS", OwnersAliases: ".OWNERS_ALIASES", 126 }, 127 }, 128 { 129 org: "kubernetes", 130 repo: "", 131 config: Owners{ 132 Filenames: map[string]ownersconfig.Filenames{ 133 "kubernetes": {Owners: "OWNERS", OwnersAliases: "OWNERS_ALIASES"}, 134 "kubernetes/test-infra": {Owners: ".OWNERS", OwnersAliases: ".OWNERS_ALIASES"}, 135 }, 136 }, 137 expected: ownersconfig.Filenames{ 138 Owners: "OWNERS", OwnersAliases: "OWNERS_ALIASES", 139 }, 140 }, 141 } 142 143 for _, tc := range cases { 144 cfg := Configuration{ 145 Owners: tc.config, 146 } 147 actual := cfg.OwnersFilenames(tc.org, tc.repo) 148 if actual != tc.expected { 149 t.Errorf("%s/%s: unexpected value. Diff: %v", tc.org, tc.repo, diff.ObjectDiff(actual, tc.expected)) 150 } 151 } 152 } 153 154 func TestSetDefault_Maps(t *testing.T) { 155 cases := []struct { 156 name string 157 config ConfigUpdater 158 expected map[string]ConfigMapSpec 159 }{ 160 { 161 name: "nothing", 162 expected: map[string]ConfigMapSpec{ 163 "config/prow/config.yaml": {Name: "config", Clusters: map[string][]string{"default": {""}}}, 164 "config/prow/plugins.yaml": {Name: "plugins", Clusters: map[string][]string{"default": {""}}}, 165 }, 166 }, 167 { 168 name: "basic", 169 config: ConfigUpdater{ 170 Maps: map[string]ConfigMapSpec{ 171 "hello.yaml": {Name: "my-cm"}, 172 "world.yaml": {Name: "you-cm"}, 173 }, 174 }, 175 expected: map[string]ConfigMapSpec{ 176 "hello.yaml": {Name: "my-cm", Clusters: map[string][]string{"default": {""}}}, 177 "world.yaml": {Name: "you-cm", Clusters: map[string][]string{"default": {""}}}, 178 }, 179 }, 180 { 181 name: "both current and deprecated", 182 config: ConfigUpdater{ 183 Maps: map[string]ConfigMapSpec{ 184 "config.yaml": {Name: "overwrite-config"}, 185 "plugins.yaml": {Name: "overwrite-plugins"}, 186 "unconflicting.yaml": {Name: "ignored"}, 187 }, 188 }, 189 expected: map[string]ConfigMapSpec{ 190 "config.yaml": {Name: "overwrite-config", Clusters: map[string][]string{"default": {""}}}, 191 "plugins.yaml": {Name: "overwrite-plugins", Clusters: map[string][]string{"default": {""}}}, 192 "unconflicting.yaml": {Name: "ignored", Clusters: map[string][]string{"default": {""}}}, 193 }, 194 }, 195 } 196 for _, tc := range cases { 197 cfg := Configuration{ 198 ConfigUpdater: tc.config, 199 } 200 cfg.setDefaults() 201 actual := cfg.ConfigUpdater.Maps 202 if len(actual) != len(tc.expected) { 203 t.Errorf("%s: actual and expected have different keys: %v %v", tc.name, actual, tc.expected) 204 continue 205 } 206 for k, n := range tc.expected { 207 if an := actual[k]; !reflect.DeepEqual(an, n) { 208 t.Errorf("%s - %s: unexpected value. Diff: %v", tc.name, k, diff.ObjectReflectDiff(an, n)) 209 } 210 } 211 } 212 } 213 214 func TestTriggerFor(t *testing.T) { 215 config := Configuration{ 216 Triggers: []Trigger{ 217 { 218 Repos: []string{"kuber"}, 219 TrustedOrg: "org1", 220 }, 221 { 222 Repos: []string{"k8s/k8s", "k8s/kuber"}, 223 TrustedOrg: "org2", 224 }, 225 { 226 Repos: []string{"k8s/t-i"}, 227 TrustedOrg: "org3", 228 }, 229 { 230 Repos: []string{"kuber/utils"}, 231 TrustedOrg: "org4", 232 }, 233 }, 234 } 235 config.setDefaults() 236 237 testCases := []struct { 238 name string 239 org, repo string 240 expectedTrusted string 241 check func(Trigger) error 242 }{ 243 { 244 name: "org trigger", 245 org: "kuber", 246 repo: "kuber", 247 expectedTrusted: "org1", 248 }, 249 { 250 name: "repo trigger", 251 org: "k8s", 252 repo: "t-i", 253 expectedTrusted: "org3", 254 }, 255 { 256 name: "repo trigger", 257 org: "kuber", 258 repo: "utils", 259 expectedTrusted: "org4", 260 }, 261 { 262 name: "default trigger", 263 org: "other", 264 repo: "other", 265 }, 266 } 267 for i := range testCases { 268 tc := testCases[i] 269 t.Run(tc.name, func(t *testing.T) { 270 actual := config.TriggerFor(tc.org, tc.repo) 271 if tc.expectedTrusted != actual.TrustedOrg { 272 t.Errorf("expected TrustedOrg to be %q, but got %q", tc.expectedTrusted, actual.TrustedOrg) 273 } 274 }) 275 } 276 } 277 278 func TestSetApproveDefaults(t *testing.T) { 279 c := &Configuration{ 280 Approve: []Approve{ 281 { 282 Repos: []string{ 283 "kubernetes/kubernetes", 284 "kubernetes-client", 285 }, 286 }, 287 { 288 Repos: []string{ 289 "kubernetes-sigs/cluster-api", 290 }, 291 CommandHelpLink: "https://prow.k8s.io/command-help", 292 PrProcessLink: "https://github.com/kubernetes/community/blob/427ccfbc7d423d8763ed756f3b8c888b7de3cf34/contributors/guide/pull-requests.md", 293 }, 294 }, 295 } 296 297 tests := []struct { 298 name string 299 org string 300 repo string 301 expectedCommandHelpLink string 302 expectedPrProcessLink string 303 }{ 304 { 305 name: "default", 306 org: "kubernetes", 307 repo: "kubernetes", 308 expectedCommandHelpLink: "https://go.k8s.io/bot-commands", 309 expectedPrProcessLink: "https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process", 310 }, 311 { 312 name: "overwrite", 313 org: "kubernetes-sigs", 314 repo: "cluster-api", 315 expectedCommandHelpLink: "https://prow.k8s.io/command-help", 316 expectedPrProcessLink: "https://github.com/kubernetes/community/blob/427ccfbc7d423d8763ed756f3b8c888b7de3cf34/contributors/guide/pull-requests.md", 317 }, 318 { 319 name: "default for repo without approve plugin config", 320 org: "kubernetes", 321 repo: "website", 322 expectedCommandHelpLink: "https://go.k8s.io/bot-commands", 323 expectedPrProcessLink: "https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process", 324 }, 325 } 326 327 for _, test := range tests { 328 329 a := c.ApproveFor(test.org, test.repo) 330 331 if a.CommandHelpLink != test.expectedCommandHelpLink { 332 t.Errorf("unexpected commandHelpLink: %s, expected: %s", a.CommandHelpLink, test.expectedCommandHelpLink) 333 } 334 335 if a.PrProcessLink != test.expectedPrProcessLink { 336 t.Errorf("unexpected prProcessLink: %s, expected: %s", a.PrProcessLink, test.expectedPrProcessLink) 337 } 338 } 339 } 340 341 func TestSetHelpDefaults(t *testing.T) { 342 tests := []struct { 343 name string 344 helpGuidelinesURL string 345 346 expectedHelpGuidelinesURL string 347 }{ 348 { 349 name: "default", 350 helpGuidelinesURL: "", 351 expectedHelpGuidelinesURL: "https://git.k8s.io/community/contributors/guide/help-wanted.md", 352 }, 353 { 354 name: "overwrite", 355 helpGuidelinesURL: "https://github.com/kubernetes/community/blob/master/contributors/guide/help-wanted.md", 356 expectedHelpGuidelinesURL: "https://github.com/kubernetes/community/blob/master/contributors/guide/help-wanted.md", 357 }, 358 } 359 360 for _, test := range tests { 361 c := &Configuration{ 362 Help: Help{ 363 HelpGuidelinesURL: test.helpGuidelinesURL, 364 }, 365 } 366 367 c.setDefaults() 368 369 if c.Help.HelpGuidelinesURL != test.expectedHelpGuidelinesURL { 370 t.Errorf("unexpected help_guidelines_url: %s, expected: %s", c.Help.HelpGuidelinesURL, test.expectedHelpGuidelinesURL) 371 } 372 } 373 } 374 375 func TestSetTriggerDefaults(t *testing.T) { 376 tests := []struct { 377 name string 378 379 trustedOrg string 380 joinOrgURL string 381 382 expectedTrustedOrg string 383 expectedJoinOrgURL string 384 }{ 385 { 386 name: "url defaults to org", 387 388 trustedOrg: "kubernetes", 389 joinOrgURL: "", 390 391 expectedTrustedOrg: "kubernetes", 392 expectedJoinOrgURL: "https://github.com/orgs/kubernetes/people", 393 }, 394 { 395 name: "both org and url are set", 396 397 trustedOrg: "kubernetes", 398 joinOrgURL: "https://git.k8s.io/community/community-membership.md#member", 399 400 expectedTrustedOrg: "kubernetes", 401 expectedJoinOrgURL: "https://git.k8s.io/community/community-membership.md#member", 402 }, 403 { 404 name: "only url is set", 405 406 trustedOrg: "", 407 joinOrgURL: "https://git.k8s.io/community/community-membership.md#member", 408 409 expectedTrustedOrg: "", 410 expectedJoinOrgURL: "https://git.k8s.io/community/community-membership.md#member", 411 }, 412 { 413 name: "nothing is set", 414 415 trustedOrg: "", 416 joinOrgURL: "", 417 418 expectedTrustedOrg: "", 419 expectedJoinOrgURL: "", 420 }, 421 } 422 423 for _, test := range tests { 424 c := &Configuration{ 425 Triggers: []Trigger{ 426 { 427 TrustedOrg: test.trustedOrg, 428 JoinOrgURL: test.joinOrgURL, 429 }, 430 }, 431 } 432 433 c.setDefaults() 434 435 if c.Triggers[0].TrustedOrg != test.expectedTrustedOrg { 436 t.Errorf("unexpected trusted_org: %s, expected: %s", c.Triggers[0].TrustedOrg, test.expectedTrustedOrg) 437 } 438 if c.Triggers[0].JoinOrgURL != test.expectedJoinOrgURL { 439 t.Errorf("unexpected join_org_url: %s, expected: %s", c.Triggers[0].JoinOrgURL, test.expectedJoinOrgURL) 440 } 441 } 442 } 443 444 func TestSetCherryPickUnapprovedDefaults(t *testing.T) { 445 defaultBranchRegexp := `^release-.*$` 446 defaultComment := `This PR is not for the master branch but does not have the ` + "`cherry-pick-approved`" + ` label. Adding the ` + "`do-not-merge/cherry-pick-not-approved`" + ` label.` 447 448 testcases := []struct { 449 name string 450 451 branchRegexp string 452 comment string 453 454 expectedBranchRegexp string 455 expectedComment string 456 }{ 457 { 458 name: "none of branchRegexp and comment are set", 459 branchRegexp: "", 460 comment: "", 461 expectedBranchRegexp: defaultBranchRegexp, 462 expectedComment: defaultComment, 463 }, 464 { 465 name: "only branchRegexp is set", 466 branchRegexp: `release-1.1.*$`, 467 comment: "", 468 expectedBranchRegexp: `release-1.1.*$`, 469 expectedComment: defaultComment, 470 }, 471 { 472 name: "only comment is set", 473 branchRegexp: "", 474 comment: "custom comment", 475 expectedBranchRegexp: defaultBranchRegexp, 476 expectedComment: "custom comment", 477 }, 478 { 479 name: "both branchRegexp and comment are set", 480 branchRegexp: `release-1.1.*$`, 481 comment: "custom comment", 482 expectedBranchRegexp: `release-1.1.*$`, 483 expectedComment: "custom comment", 484 }, 485 } 486 487 for _, tc := range testcases { 488 c := &Configuration{ 489 CherryPickUnapproved: CherryPickUnapproved{ 490 BranchRegexp: tc.branchRegexp, 491 Comment: tc.comment, 492 }, 493 } 494 495 c.setDefaults() 496 497 if c.CherryPickUnapproved.BranchRegexp != tc.expectedBranchRegexp { 498 t.Errorf("unexpected branchRegexp: %s, expected: %s", c.CherryPickUnapproved.BranchRegexp, tc.expectedBranchRegexp) 499 } 500 if c.CherryPickUnapproved.Comment != tc.expectedComment { 501 t.Errorf("unexpected comment: %s, expected: %s", c.CherryPickUnapproved.Comment, tc.expectedComment) 502 } 503 } 504 } 505 506 func TestOptionsForItem(t *testing.T) { 507 open := true 508 one, two := "v1", "v2" 509 var testCases = []struct { 510 name string 511 item string 512 config map[string]BugzillaBranchOptions 513 expected BugzillaBranchOptions 514 }{ 515 { 516 name: "no config means no options", 517 item: "item", 518 config: map[string]BugzillaBranchOptions{}, 519 expected: BugzillaBranchOptions{}, 520 }, 521 { 522 name: "unrelated config means no options", 523 item: "item", 524 config: map[string]BugzillaBranchOptions{"other": {IsOpen: &open, TargetRelease: &one}}, 525 expected: BugzillaBranchOptions{}, 526 }, 527 { 528 name: "global config resolves to options", 529 item: "item", 530 config: map[string]BugzillaBranchOptions{"*": {IsOpen: &open, TargetRelease: &one}}, 531 expected: BugzillaBranchOptions{IsOpen: &open, TargetRelease: &one}, 532 }, 533 { 534 name: "specific config resolves to options", 535 item: "item", 536 config: map[string]BugzillaBranchOptions{"item": {IsOpen: &open, TargetRelease: &one}}, 537 expected: BugzillaBranchOptions{IsOpen: &open, TargetRelease: &one}, 538 }, 539 { 540 name: "global and specific config resolves to options that favor specificity", 541 item: "item", 542 config: map[string]BugzillaBranchOptions{ 543 "*": {IsOpen: &open, TargetRelease: &one}, 544 "item": {TargetRelease: &two}, 545 }, 546 expected: BugzillaBranchOptions{IsOpen: &open, TargetRelease: &two}, 547 }, 548 } 549 550 for _, testCase := range testCases { 551 t.Run(testCase.name, func(t *testing.T) { 552 if actual, expected := OptionsForItem(testCase.item, testCase.config), testCase.expected; !reflect.DeepEqual(actual, expected) { 553 t.Errorf("%s: got incorrect options for item %q: %v", testCase.name, testCase.item, diff.ObjectReflectDiff(actual, expected)) 554 } 555 }) 556 } 557 } 558 559 func TestResolveBugzillaOptions(t *testing.T) { 560 open, closed := true, false 561 yes, no := true, false 562 one, two := "v1", "v2" 563 modified, verified, post, pre := "MODIFIED", "VERIFIED", "POST", "PRE" 564 modifiedState := BugzillaBugState{Status: modified} 565 verifiedState := BugzillaBugState{Status: verified} 566 postState := BugzillaBugState{Status: post} 567 preState := BugzillaBugState{Status: pre} 568 var testCases = []struct { 569 name string 570 parent, child BugzillaBranchOptions 571 expected BugzillaBranchOptions 572 }{ 573 { 574 name: "no parent or child means no output", 575 }, 576 { 577 name: "no child means a copy of parent is the output", 578 parent: BugzillaBranchOptions{ValidateByDefault: &yes, IsOpen: &open, TargetRelease: &one, ValidStates: &[]BugzillaBugState{modifiedState}, DependentBugStates: &[]BugzillaBugState{verifiedState}, DependentBugTargetReleases: &[]string{one}, StateAfterValidation: &postState}, 579 expected: BugzillaBranchOptions{ 580 ValidateByDefault: &yes, 581 IsOpen: &open, 582 TargetRelease: &one, 583 ValidStates: &[]BugzillaBugState{modifiedState}, 584 DependentBugStates: &[]BugzillaBugState{verifiedState}, 585 DependentBugTargetReleases: &[]string{one}, 586 StateAfterValidation: &postState, 587 }, 588 }, 589 { 590 name: "no parent means a copy of child is the output", 591 child: BugzillaBranchOptions{ValidateByDefault: &yes, IsOpen: &open, TargetRelease: &one, ValidStates: &[]BugzillaBugState{modifiedState}, DependentBugStates: &[]BugzillaBugState{verifiedState}, DependentBugTargetReleases: &[]string{one}, StateAfterValidation: &postState}, 592 expected: BugzillaBranchOptions{ 593 ValidateByDefault: &yes, 594 IsOpen: &open, 595 TargetRelease: &one, 596 ValidStates: &[]BugzillaBugState{modifiedState}, 597 DependentBugStates: &[]BugzillaBugState{verifiedState}, 598 DependentBugTargetReleases: &[]string{one}, 599 StateAfterValidation: &postState, 600 }, 601 }, 602 { 603 name: "child overrides parent on IsOpen", 604 parent: BugzillaBranchOptions{IsOpen: &open, TargetRelease: &one, ValidStates: &[]BugzillaBugState{modifiedState}, StateAfterValidation: &postState}, 605 child: BugzillaBranchOptions{IsOpen: &closed}, 606 expected: BugzillaBranchOptions{IsOpen: &closed, TargetRelease: &one, ValidStates: &[]BugzillaBugState{modifiedState}, StateAfterValidation: &postState}, 607 }, 608 { 609 name: "child overrides parent on target release", 610 parent: BugzillaBranchOptions{IsOpen: &open, TargetRelease: &one, ValidStates: &[]BugzillaBugState{modifiedState}, StateAfterValidation: &postState}, 611 child: BugzillaBranchOptions{TargetRelease: &two}, 612 expected: BugzillaBranchOptions{IsOpen: &open, TargetRelease: &two, ValidStates: &[]BugzillaBugState{modifiedState}, StateAfterValidation: &postState}, 613 }, 614 { 615 name: "child overrides parent on states", 616 parent: BugzillaBranchOptions{IsOpen: &open, TargetRelease: &one, ValidStates: &[]BugzillaBugState{modifiedState}, StateAfterValidation: &postState}, 617 child: BugzillaBranchOptions{ValidStates: &[]BugzillaBugState{verifiedState}}, 618 expected: BugzillaBranchOptions{IsOpen: &open, TargetRelease: &one, ValidStates: &[]BugzillaBugState{verifiedState}, StateAfterValidation: &postState}, 619 }, 620 { 621 name: "child overrides parent on state after validation", 622 parent: BugzillaBranchOptions{IsOpen: &open, TargetRelease: &one, ValidStates: &[]BugzillaBugState{modifiedState}, StateAfterValidation: &postState}, 623 child: BugzillaBranchOptions{StateAfterValidation: &preState}, 624 expected: BugzillaBranchOptions{IsOpen: &open, TargetRelease: &one, ValidStates: &[]BugzillaBugState{modifiedState}, StateAfterValidation: &preState}, 625 }, 626 { 627 name: "child overrides parent on validation by default", 628 parent: BugzillaBranchOptions{IsOpen: &open, TargetRelease: &one, ValidStates: &[]BugzillaBugState{modifiedState}, StateAfterValidation: &postState}, 629 child: BugzillaBranchOptions{ValidateByDefault: &yes}, 630 expected: BugzillaBranchOptions{ValidateByDefault: &yes, IsOpen: &open, TargetRelease: &one, ValidStates: &[]BugzillaBugState{modifiedState}, StateAfterValidation: &postState}, 631 }, 632 { 633 name: "child overrides parent on dependent bug states", 634 parent: BugzillaBranchOptions{IsOpen: &open, TargetRelease: &one, ValidStates: &[]BugzillaBugState{modifiedState}, DependentBugStates: &[]BugzillaBugState{verifiedState}, StateAfterValidation: &postState}, 635 child: BugzillaBranchOptions{DependentBugStates: &[]BugzillaBugState{modifiedState}}, 636 expected: BugzillaBranchOptions{ 637 IsOpen: &open, 638 TargetRelease: &one, 639 ValidStates: &[]BugzillaBugState{modifiedState}, 640 DependentBugStates: &[]BugzillaBugState{modifiedState}, 641 StateAfterValidation: &postState, 642 }, 643 }, 644 { 645 name: "child overrides parent on dependent bug target releases", 646 parent: BugzillaBranchOptions{IsOpen: &open, TargetRelease: &one, ValidStates: &[]BugzillaBugState{modifiedState}, StateAfterValidation: &postState, DependentBugTargetReleases: &[]string{one}}, 647 child: BugzillaBranchOptions{DependentBugTargetReleases: &[]string{two}}, 648 expected: BugzillaBranchOptions{IsOpen: &open, TargetRelease: &one, ValidStates: &[]BugzillaBugState{modifiedState}, StateAfterValidation: &postState, DependentBugTargetReleases: &[]string{two}}, 649 }, 650 { 651 name: "child overrides parent on state after merge", 652 parent: BugzillaBranchOptions{IsOpen: &open, TargetRelease: &one, ValidStates: &[]BugzillaBugState{modifiedState}, StateAfterValidation: &postState, StateAfterMerge: &postState}, 653 child: BugzillaBranchOptions{StateAfterMerge: &preState}, 654 expected: BugzillaBranchOptions{ 655 IsOpen: &open, 656 TargetRelease: &one, 657 ValidStates: &[]BugzillaBugState{modifiedState}, 658 StateAfterValidation: &postState, 659 StateAfterMerge: &preState, 660 }, 661 }, 662 { 663 name: "status slices are correctly merged with states slices on parent", 664 parent: BugzillaBranchOptions{Statuses: &[]string{modified}, ValidStates: &[]BugzillaBugState{verifiedState}, DependentBugStatuses: &[]string{pre}, DependentBugStates: &[]BugzillaBugState{postState}}, 665 expected: BugzillaBranchOptions{ValidStates: &[]BugzillaBugState{modifiedState, verifiedState}, DependentBugStates: &[]BugzillaBugState{postState, preState}}, 666 }, 667 { 668 name: "status slices are correctly merged with states slices on child", 669 child: BugzillaBranchOptions{Statuses: &[]string{modified}, ValidStates: &[]BugzillaBugState{verifiedState}, DependentBugStatuses: &[]string{pre}, DependentBugStates: &[]BugzillaBugState{postState}}, 670 expected: BugzillaBranchOptions{ValidStates: &[]BugzillaBugState{modifiedState, verifiedState}, DependentBugStates: &[]BugzillaBugState{postState, preState}}, 671 }, 672 { 673 name: "state fields when not present re inferred from status fields on parent", 674 parent: BugzillaBranchOptions{StatusAfterMerge: &modified, StatusAfterValidation: &verified}, 675 expected: BugzillaBranchOptions{StateAfterMerge: &modifiedState, StateAfterValidation: &verifiedState}, 676 }, 677 { 678 name: "state fields when not present are inferred from status fields on child", 679 child: BugzillaBranchOptions{StatusAfterMerge: &modified, StatusAfterValidation: &verified}, 680 expected: BugzillaBranchOptions{StateAfterMerge: &modifiedState, StateAfterValidation: &verifiedState}, 681 }, 682 { 683 name: "child status overrides all statuses and states of the parent", 684 parent: BugzillaBranchOptions{Statuses: &[]string{modified}, ValidStates: &[]BugzillaBugState{verifiedState}, DependentBugStatuses: &[]string{modified}, DependentBugStates: &[]BugzillaBugState{verifiedState}, StatusAfterMerge: &pre, StateAfterMerge: &preState, StatusAfterValidation: &pre, StateAfterValidation: &preState}, 685 child: BugzillaBranchOptions{Statuses: &[]string{post}, DependentBugStatuses: &[]string{post}, StatusAfterMerge: &post, StatusAfterValidation: &post}, 686 expected: BugzillaBranchOptions{ValidStates: &[]BugzillaBugState{postState}, DependentBugStates: &[]BugzillaBugState{postState}, StateAfterMerge: &postState, StateAfterValidation: &postState}, 687 }, 688 { 689 name: "parent dependent target release is merged on child", 690 parent: BugzillaBranchOptions{DeprecatedDependentBugTargetRelease: &one}, 691 child: BugzillaBranchOptions{}, 692 expected: BugzillaBranchOptions{DependentBugTargetReleases: &[]string{one}}, 693 }, 694 { 695 name: "parent dependent target release is merged into target releases", 696 parent: BugzillaBranchOptions{DependentBugTargetReleases: &[]string{one}, DeprecatedDependentBugTargetRelease: &two}, 697 child: BugzillaBranchOptions{}, 698 expected: BugzillaBranchOptions{DependentBugTargetReleases: &[]string{one, two}}, 699 }, 700 { 701 name: "child overrides parent on all fields", 702 parent: BugzillaBranchOptions{ValidateByDefault: &yes, IsOpen: &open, TargetRelease: &one, ValidStates: &[]BugzillaBugState{verifiedState}, DependentBugStates: &[]BugzillaBugState{verifiedState}, DependentBugTargetReleases: &[]string{one}, StateAfterValidation: &postState, StateAfterMerge: &postState}, 703 child: BugzillaBranchOptions{ValidateByDefault: &no, IsOpen: &closed, TargetRelease: &two, ValidStates: &[]BugzillaBugState{modifiedState}, DependentBugStates: &[]BugzillaBugState{modifiedState}, DependentBugTargetReleases: &[]string{two}, StateAfterValidation: &preState, StateAfterMerge: &preState}, 704 expected: BugzillaBranchOptions{ 705 ValidateByDefault: &no, 706 IsOpen: &closed, 707 TargetRelease: &two, 708 ValidStates: &[]BugzillaBugState{modifiedState}, 709 DependentBugStates: &[]BugzillaBugState{modifiedState}, 710 DependentBugTargetReleases: &[]string{two}, 711 StateAfterValidation: &preState, 712 StateAfterMerge: &preState, 713 }, 714 }, 715 { 716 name: "parent target release is excluded on child", 717 parent: BugzillaBranchOptions{TargetRelease: &one}, 718 child: BugzillaBranchOptions{ExcludeDefaults: &yes}, 719 expected: BugzillaBranchOptions{ExcludeDefaults: &yes}, 720 }, 721 { 722 name: "parent target release is excluded on child with other options", 723 parent: BugzillaBranchOptions{DependentBugTargetReleases: &[]string{one}}, 724 child: BugzillaBranchOptions{TargetRelease: &one, ExcludeDefaults: &yes}, 725 expected: BugzillaBranchOptions{TargetRelease: &one, ExcludeDefaults: &yes}, 726 }, 727 { 728 name: "parent exclude merges with child options", 729 parent: BugzillaBranchOptions{DependentBugTargetReleases: &[]string{one}, ExcludeDefaults: &yes}, 730 child: BugzillaBranchOptions{TargetRelease: &one}, 731 expected: BugzillaBranchOptions{DependentBugTargetReleases: &[]string{one}, TargetRelease: &one, ExcludeDefaults: &yes}, 732 }, 733 } 734 for _, testCase := range testCases { 735 t.Run(testCase.name, func(t *testing.T) { 736 if actual, expected := ResolveBugzillaOptions(testCase.parent, testCase.child), testCase.expected; !reflect.DeepEqual(actual, expected) { 737 t.Errorf("%s: resolved incorrect options for parent and child: %v", testCase.name, diff.ObjectReflectDiff(actual, expected)) 738 } 739 }) 740 } 741 742 var i int = 0 743 managedCol1 := ManagedColumn{ID: &i, Name: "col1", State: "open", Labels: []string{"area/conformance", "area/testing"}, Org: "org1"} 744 managedCol3 := ManagedColumn{ID: &i, Name: "col2", State: "open", Labels: []string{}, Org: "org2"} 745 managedColx := ManagedColumn{ID: &i, Name: "col2", State: "open", Labels: []string{"area/conformance", "area/testing"}, Org: "org2"} 746 invalidCol := ManagedColumn{State: "open", Labels: []string{"area/conformance", "area/testing2"}, Org: "org2"} 747 invalidOrg := ManagedColumn{Name: "col1", State: "open", Labels: []string{"area/conformance", "area/testing2"}, Org: ""} 748 managedProj2 := ManagedProject{Columns: []ManagedColumn{managedCol3}} 749 managedProjx := ManagedProject{Columns: []ManagedColumn{managedCol1, managedColx}} 750 managedOrgRepo2 := ManagedOrgRepo{Projects: map[string]ManagedProject{"project1": managedProj2}} 751 managedOrgRepox := ManagedOrgRepo{Projects: map[string]ManagedProject{"project1": managedProjx}} 752 753 projectManagerTestcases := []struct { 754 name string 755 config *Configuration 756 expectedErr string 757 }{ 758 { 759 name: "No projects configured in a repo", 760 config: &Configuration{ 761 ProjectManager: ProjectManager{ 762 OrgRepos: map[string]ManagedOrgRepo{"org1": {Projects: map[string]ManagedProject{}}}, 763 }, 764 }, 765 expectedErr: fmt.Sprintf("Org/repo: %s, has no projects configured", "org1"), 766 }, 767 { 768 name: "No columns configured for a project", 769 config: &Configuration{ 770 ProjectManager: ProjectManager{ 771 OrgRepos: map[string]ManagedOrgRepo{"org1": {Projects: map[string]ManagedProject{"project1": {Columns: []ManagedColumn{}}}}}, 772 }, 773 }, 774 expectedErr: fmt.Sprintf("Org/repo: %s, project %s, has no columns configured", "org1", "project1"), 775 }, 776 { 777 name: "Columns does not have name or ID", 778 config: &Configuration{ 779 ProjectManager: ProjectManager{ 780 OrgRepos: map[string]ManagedOrgRepo{"org1": {Projects: map[string]ManagedProject{"project1": {Columns: []ManagedColumn{invalidCol}}}}}, 781 }, 782 }, 783 expectedErr: fmt.Sprintf("Org/repo: %s, project %s, column %v, has no name/id configured", "org1", "project1", invalidCol), 784 }, 785 { 786 name: "Columns does not have owner Org/repo", 787 config: &Configuration{ 788 ProjectManager: ProjectManager{ 789 OrgRepos: map[string]ManagedOrgRepo{"org1": {Projects: map[string]ManagedProject{"project1": {Columns: []ManagedColumn{invalidOrg}}}}}, 790 }, 791 }, 792 expectedErr: fmt.Sprintf("Org/repo: %s, project %s, column %s, has no org configured", "org1", "project1", "col1"), 793 }, 794 { 795 name: "No Labels specified in the column of the project", 796 config: &Configuration{ 797 ProjectManager: ProjectManager{ 798 OrgRepos: map[string]ManagedOrgRepo{"org1": managedOrgRepo2}, 799 }, 800 }, 801 expectedErr: fmt.Sprintf("Org/repo: %s, project %s, column %s, has no labels configured", "org1", "project1", "col2"), 802 }, 803 { 804 name: "Same Label specified to multiple column in a project", 805 config: &Configuration{ 806 ProjectManager: ProjectManager{ 807 OrgRepos: map[string]ManagedOrgRepo{"org1": managedOrgRepox}, 808 }, 809 }, 810 expectedErr: fmt.Sprintf("Org/repo: %s, project %s, column %s has same labels configured as another column", "org1", "project1", "col2"), 811 }, 812 } 813 814 for _, c := range projectManagerTestcases { 815 t.Run(c.name, func(t *testing.T) { 816 err := validateProjectManager(c.config.ProjectManager) 817 if err != nil && len(c.expectedErr) == 0 { 818 t.Fatalf("config validation error: %v", err) 819 } 820 if err == nil && len(c.expectedErr) > 0 { 821 t.Fatalf("config validation error: %v but expecting %v", err, c.expectedErr) 822 } 823 if err != nil && c.expectedErr != err.Error() { 824 t.Fatalf("Error running the test %s, \nexpected: %s, \nreceived: %s", c.name, c.expectedErr, err.Error()) 825 } 826 }) 827 } 828 } 829 830 func TestOptionsForBranch(t *testing.T) { 831 open, closed := true, false 832 yes, no := true, false 833 globalDefault, globalBranchDefault, orgDefault, orgBranchDefault, repoDefault, repoBranch, legacyBranch := "global-default", "global-branch-default", "my-org-default", "my-org-branch-default", "my-repo-default", "my-repo-branch", "my-legacy-branch" 834 post, pre, release, notabug, new, reset := "POST", "PRE", "RELEASE_PENDING", "NOTABUG", "NEW", "RESET" 835 verifiedState, modifiedState := BugzillaBugState{Status: "VERIFIED"}, BugzillaBugState{Status: "MODIFIED"} 836 postState, preState, releaseState, notabugState, newState, resetState := BugzillaBugState{Status: post}, BugzillaBugState{Status: pre}, BugzillaBugState{Status: release}, BugzillaBugState{Status: notabug}, BugzillaBugState{Status: new}, BugzillaBugState{Status: reset} 837 closedErrata := BugzillaBugState{Status: "CLOSED", Resolution: "ERRATA"} 838 orgAllowedGroups, repoAllowedGroups := []string{"test"}, []string{"security", "test"} 839 840 rawConfig := `default: 841 "*": 842 target_release: global-default 843 "global-branch": 844 is_open: false 845 target_release: global-branch-default 846 orgs: 847 my-org: 848 default: 849 "*": 850 is_open: true 851 target_release: my-org-default 852 state_after_validation: 853 status: "PRE" 854 state_after_close: 855 status: "NEW" 856 allowed_groups: 857 - test 858 "my-org-branch": 859 target_release: my-org-branch-default 860 state_after_validation: 861 status: "POST" 862 repos: 863 my-repo: 864 branches: 865 "*": 866 is_open: false 867 target_release: my-repo-default 868 valid_states: 869 - status: VERIFIED 870 validate_by_default: false 871 state_after_merge: 872 status: RELEASE_PENDING 873 "my-repo-branch": 874 target_release: my-repo-branch 875 valid_states: 876 - status: MODIFIED 877 - status: CLOSED 878 resolution: ERRATA 879 validate_by_default: true 880 state_after_merge: 881 status: NOTABUG 882 state_after_close: 883 status: RESET 884 allowed_groups: 885 - security 886 "my-legacy-branch": 887 target_release: my-legacy-branch 888 statuses: 889 - MODIFIED 890 dependent_bug_statuses: 891 - VERIFIED 892 validate_by_default: true 893 status_after_validation: MODIFIED 894 status_after_merge: NOTABUG 895 "my-special-branch": 896 exclude_defaults: true 897 validate_by_default: false 898 another-repo: 899 branches: 900 "*": 901 exclude_defaults: true 902 "my-org-branch": 903 target_release: my-repo-branch` 904 var config Bugzilla 905 if err := yaml.Unmarshal([]byte(rawConfig), &config); err != nil { 906 t.Fatalf("couldn't unmarshal config: %v", err) 907 } 908 909 var testCases = []struct { 910 name string 911 org, repo, branch string 912 expected BugzillaBranchOptions 913 }{ 914 { 915 name: "unconfigured branch gets global default", 916 org: "some-org", 917 repo: "some-repo", 918 branch: "some-branch", 919 expected: BugzillaBranchOptions{TargetRelease: &globalDefault}, 920 }, 921 { 922 name: "branch on unconfigured org/repo gets global default", 923 org: "some-org", 924 repo: "some-repo", 925 branch: "global-branch", 926 expected: BugzillaBranchOptions{IsOpen: &closed, TargetRelease: &globalBranchDefault}, 927 }, 928 { 929 name: "branch on configured org but not repo gets org default", 930 org: "my-org", 931 repo: "some-repo", 932 branch: "some-branch", 933 expected: BugzillaBranchOptions{IsOpen: &open, TargetRelease: &orgDefault, StateAfterValidation: &preState, AllowedGroups: orgAllowedGroups, StateAfterClose: &newState}, 934 }, 935 { 936 name: "branch on configured org but not repo gets org branch default", 937 org: "my-org", 938 repo: "some-repo", 939 branch: "my-org-branch", 940 expected: BugzillaBranchOptions{IsOpen: &open, TargetRelease: &orgBranchDefault, StateAfterValidation: &postState, AllowedGroups: orgAllowedGroups, StateAfterClose: &newState}, 941 }, 942 { 943 name: "branch on configured org and repo gets repo default", 944 org: "my-org", 945 repo: "my-repo", 946 branch: "some-branch", 947 expected: BugzillaBranchOptions{ValidateByDefault: &no, IsOpen: &closed, TargetRelease: &repoDefault, ValidStates: &[]BugzillaBugState{verifiedState}, StateAfterValidation: &preState, StateAfterMerge: &releaseState, AllowedGroups: orgAllowedGroups, StateAfterClose: &newState}, 948 }, 949 { 950 name: "branch on configured org and repo gets branch config", 951 org: "my-org", 952 repo: "my-repo", 953 branch: "my-repo-branch", 954 expected: BugzillaBranchOptions{ValidateByDefault: &yes, IsOpen: &closed, TargetRelease: &repoBranch, ValidStates: &[]BugzillaBugState{modifiedState, closedErrata}, StateAfterValidation: &preState, StateAfterMerge: ¬abugState, AllowedGroups: repoAllowedGroups, StateAfterClose: &resetState}, 955 }, 956 { 957 name: "exclude branch on configured org and repo gets branch config", 958 org: "my-org", 959 repo: "my-repo", 960 branch: "my-special-branch", 961 expected: BugzillaBranchOptions{ValidateByDefault: &no, ExcludeDefaults: &yes}, 962 }, 963 { 964 name: "exclude branch on repo cascades to branch config", 965 org: "my-org", 966 repo: "another-repo", 967 branch: "my-org-branch", 968 expected: BugzillaBranchOptions{TargetRelease: &repoBranch, ExcludeDefaults: &yes}, 969 }, 970 } 971 for _, testCase := range testCases { 972 t.Run(testCase.name, func(t *testing.T) { 973 if actual, expected := config.OptionsForBranch(testCase.org, testCase.repo, testCase.branch), testCase.expected; !reflect.DeepEqual(actual, expected) { 974 t.Errorf("%s: resolved incorrect options for %s/%s#%s: %v", testCase.name, testCase.org, testCase.repo, testCase.branch, diff.ObjectReflectDiff(actual, expected)) 975 } 976 }) 977 } 978 979 var repoTestCases = []struct { 980 name string 981 org, repo string 982 expected map[string]BugzillaBranchOptions 983 }{ 984 { 985 name: "unconfigured repo gets global default", 986 org: "some-org", 987 repo: "some-repo", 988 expected: map[string]BugzillaBranchOptions{ 989 "*": {TargetRelease: &globalDefault}, 990 "global-branch": {IsOpen: &closed, TargetRelease: &globalBranchDefault}, 991 }, 992 }, 993 { 994 name: "repo in configured org gets org default", 995 org: "my-org", 996 repo: "some-repo", 997 expected: map[string]BugzillaBranchOptions{ 998 "*": {IsOpen: &open, TargetRelease: &orgDefault, StateAfterValidation: &preState, AllowedGroups: orgAllowedGroups, StateAfterClose: &newState}, 999 "my-org-branch": {IsOpen: &open, TargetRelease: &orgBranchDefault, StateAfterValidation: &postState, AllowedGroups: orgAllowedGroups, StateAfterClose: &newState}, 1000 }, 1001 }, 1002 { 1003 name: "configured repo gets repo config", 1004 org: "my-org", 1005 repo: "my-repo", 1006 expected: map[string]BugzillaBranchOptions{ 1007 "*": { 1008 ValidateByDefault: &no, 1009 IsOpen: &closed, 1010 TargetRelease: &repoDefault, 1011 ValidStates: &[]BugzillaBugState{verifiedState}, 1012 StateAfterValidation: &preState, 1013 StateAfterMerge: &releaseState, 1014 AllowedGroups: orgAllowedGroups, 1015 StateAfterClose: &newState, 1016 }, 1017 "my-repo-branch": { 1018 ValidateByDefault: &yes, 1019 IsOpen: &closed, 1020 TargetRelease: &repoBranch, 1021 ValidStates: &[]BugzillaBugState{modifiedState, closedErrata}, 1022 StateAfterValidation: &preState, 1023 StateAfterMerge: ¬abugState, 1024 AllowedGroups: repoAllowedGroups, 1025 StateAfterClose: &resetState, 1026 }, 1027 "my-org-branch": { 1028 ValidateByDefault: &no, 1029 IsOpen: &closed, 1030 TargetRelease: &repoDefault, 1031 ValidStates: &[]BugzillaBugState{verifiedState}, 1032 StateAfterValidation: &postState, 1033 StateAfterMerge: &releaseState, 1034 AllowedGroups: orgAllowedGroups, 1035 StateAfterClose: &newState, 1036 }, 1037 "my-legacy-branch": { 1038 ValidateByDefault: &yes, 1039 IsOpen: &closed, 1040 TargetRelease: &legacyBranch, 1041 ValidStates: &[]BugzillaBugState{modifiedState}, 1042 DependentBugStates: &[]BugzillaBugState{verifiedState}, 1043 StateAfterValidation: &modifiedState, 1044 StateAfterMerge: ¬abugState, 1045 AllowedGroups: orgAllowedGroups, 1046 StateAfterClose: &newState, 1047 }, 1048 "my-special-branch": { 1049 ValidateByDefault: &no, 1050 ExcludeDefaults: &yes, 1051 }, 1052 }, 1053 }, 1054 { 1055 name: "excluded repo gets no defaults", 1056 org: "my-org", 1057 repo: "another-repo", 1058 expected: map[string]BugzillaBranchOptions{ 1059 "*": {ExcludeDefaults: &yes}, 1060 "my-org-branch": {ExcludeDefaults: &yes, TargetRelease: &repoBranch}, 1061 }, 1062 }, 1063 } 1064 for _, testCase := range repoTestCases { 1065 t.Run(testCase.name, func(t *testing.T) { 1066 if actual, expected := config.OptionsForRepo(testCase.org, testCase.repo), testCase.expected; !reflect.DeepEqual(actual, expected) { 1067 t.Errorf("%s: resolved incorrect options for %s/%s: %v", testCase.name, testCase.org, testCase.repo, diff.ObjectReflectDiff(actual, expected)) 1068 } 1069 }) 1070 } 1071 } 1072 1073 func TestBugzillaBugState_String(t *testing.T) { 1074 testCases := []struct { 1075 name string 1076 state *BugzillaBugState 1077 expected string 1078 }{ 1079 { 1080 name: "empty struct", 1081 state: &BugzillaBugState{}, 1082 expected: "", 1083 }, 1084 { 1085 name: "only status", 1086 state: &BugzillaBugState{Status: "CLOSED"}, 1087 expected: "CLOSED", 1088 }, 1089 { 1090 name: "only resolution", 1091 state: &BugzillaBugState{Resolution: "NOTABUG"}, 1092 expected: "any status with resolution NOTABUG", 1093 }, 1094 { 1095 name: "status and resolution", 1096 state: &BugzillaBugState{Status: "CLOSED", Resolution: "NOTABUG"}, 1097 expected: "CLOSED (NOTABUG)", 1098 }, 1099 } 1100 for _, tc := range testCases { 1101 t.Run(tc.name, func(t *testing.T) { 1102 actual := tc.state.String() 1103 if actual != tc.expected { 1104 t.Errorf("%s: expected %q, got %q", tc.name, tc.expected, actual) 1105 } 1106 }) 1107 } 1108 } 1109 1110 func TestBugzillaBugState_Matches(t *testing.T) { 1111 modified, closed, errata, notabug := "MODIFIED", "CLOSED", "ERRATA", "NOTABUG" 1112 testCases := []struct { 1113 name string 1114 state *BugzillaBugState 1115 bug *bugzilla.Bug 1116 expected bool 1117 }{ 1118 { 1119 name: "both pointers are nil -> false", 1120 }, 1121 { 1122 name: "state pointer is nil -> false", 1123 bug: &bugzilla.Bug{}, 1124 }, 1125 { 1126 name: "bug pointer is nil -> false", 1127 state: &BugzillaBugState{}, 1128 }, 1129 { 1130 name: "statuses do not match -> false", 1131 state: &BugzillaBugState{Status: modified, Resolution: errata}, 1132 bug: &bugzilla.Bug{Status: closed, Resolution: errata}, 1133 expected: false, 1134 }, 1135 { 1136 name: "resolutions do not match -> false", 1137 state: &BugzillaBugState{Status: closed, Resolution: notabug}, 1138 bug: &bugzilla.Bug{Status: closed, Resolution: errata}, 1139 expected: false, 1140 }, 1141 { 1142 name: "no state enforced -> true", 1143 state: &BugzillaBugState{}, 1144 bug: &bugzilla.Bug{Status: closed, Resolution: errata}, 1145 expected: true, 1146 }, 1147 { 1148 name: "status match, resolution not enforced -> true", 1149 state: &BugzillaBugState{Status: closed}, 1150 bug: &bugzilla.Bug{Status: closed, Resolution: errata}, 1151 expected: true, 1152 }, 1153 { 1154 name: "status not enforced, resolution match -> true", 1155 state: &BugzillaBugState{Resolution: errata}, 1156 bug: &bugzilla.Bug{Status: closed, Resolution: errata}, 1157 expected: true, 1158 }, 1159 { 1160 name: "status and resolution match -> true", 1161 state: &BugzillaBugState{Status: closed, Resolution: errata}, 1162 bug: &bugzilla.Bug{Status: closed, Resolution: errata}, 1163 expected: true, 1164 }, 1165 } 1166 1167 for _, tc := range testCases { 1168 t.Run(tc.name, func(t *testing.T) { 1169 actual := tc.state.Matches(tc.bug) 1170 if actual != tc.expected { 1171 t.Errorf("%s: expected %t, got %t", tc.name, tc.expected, actual) 1172 } 1173 }) 1174 } 1175 } 1176 1177 func TestBugzillaBugState_AsBugUpdate(t *testing.T) { 1178 modified, closed, errata, notabug := "MODIFIED", "CLOSED", "ERRATA", "NOTABUG" 1179 testCases := []struct { 1180 name string 1181 state *BugzillaBugState 1182 bug *bugzilla.Bug 1183 expected *bugzilla.BugUpdate 1184 }{ 1185 { 1186 name: "bug is nil so update contains whole state", 1187 state: &BugzillaBugState{Status: closed, Resolution: errata}, 1188 expected: &bugzilla.BugUpdate{Status: closed, Resolution: errata}, 1189 }, 1190 { 1191 name: "bug is empty so update contains whole state", 1192 state: &BugzillaBugState{Status: closed, Resolution: errata}, 1193 bug: &bugzilla.Bug{}, 1194 expected: &bugzilla.BugUpdate{Status: closed, Resolution: errata}, 1195 }, 1196 { 1197 name: "state is empty so update is nil", 1198 state: &BugzillaBugState{}, 1199 bug: &bugzilla.Bug{Status: closed, Resolution: errata}, 1200 expected: nil, 1201 }, 1202 { 1203 name: "status differs so update contains it", 1204 state: &BugzillaBugState{Status: closed}, 1205 bug: &bugzilla.Bug{Status: modified, Resolution: errata}, 1206 expected: &bugzilla.BugUpdate{Status: closed}, 1207 }, 1208 { 1209 name: "resolution differs so update contains it", 1210 state: &BugzillaBugState{Status: closed, Resolution: errata}, 1211 bug: &bugzilla.Bug{Status: closed, Resolution: notabug}, 1212 expected: &bugzilla.BugUpdate{Resolution: errata}, 1213 }, 1214 { 1215 name: "status and resolution match so update is nil", 1216 state: &BugzillaBugState{Status: closed, Resolution: errata}, 1217 bug: &bugzilla.Bug{Status: closed, Resolution: errata}, 1218 expected: nil, 1219 }, 1220 } 1221 for _, tc := range testCases { 1222 t.Run(tc.name, func(t *testing.T) { 1223 actual := tc.state.AsBugUpdate(tc.bug) 1224 if tc.expected != actual { 1225 if actual == nil { 1226 t.Errorf("%s: unexpected nil", tc.name) 1227 } 1228 if tc.expected == nil { 1229 t.Errorf("%s: expected nil, got %v", tc.name, actual) 1230 } 1231 } 1232 1233 if !reflect.DeepEqual(tc.expected, actual) { 1234 t.Errorf("%s: BugUpdate differs from expected:\n%s", tc.name, diff.ObjectReflectDiff(*actual, *tc.expected)) 1235 } 1236 }) 1237 } 1238 } 1239 1240 func TestBugzillaBugStateSet_Has(t *testing.T) { 1241 bugInProgress := BugzillaBugState{Status: "MODIFIED"} 1242 bugErrata := BugzillaBugState{Status: "CLOSED", Resolution: "ERRATA"} 1243 bugWontfix := BugzillaBugState{Status: "CLOSED", Resolution: "WONTFIX"} 1244 1245 testCases := []struct { 1246 name string 1247 states []BugzillaBugState 1248 state BugzillaBugState 1249 1250 expectedLength int 1251 expectedHas bool 1252 }{ 1253 { 1254 name: "empty set", 1255 state: bugInProgress, 1256 expectedLength: 0, 1257 expectedHas: false, 1258 }, 1259 { 1260 name: "membership", 1261 states: []BugzillaBugState{bugInProgress}, 1262 state: bugInProgress, 1263 expectedLength: 1, 1264 expectedHas: true, 1265 }, 1266 { 1267 name: "non-membership", 1268 states: []BugzillaBugState{bugInProgress, bugErrata}, 1269 state: bugWontfix, 1270 expectedLength: 2, 1271 expectedHas: false, 1272 }, 1273 { 1274 name: "actually a set", 1275 states: []BugzillaBugState{bugInProgress, bugInProgress, bugInProgress}, 1276 state: bugInProgress, 1277 expectedLength: 1, 1278 expectedHas: true, 1279 }, 1280 } 1281 1282 for _, tc := range testCases { 1283 t.Run(tc.name, func(t *testing.T) { 1284 set := NewBugzillaBugStateSet(tc.states) 1285 if len(set) != tc.expectedLength { 1286 t.Errorf("%s: expected set to have %d members, it has %d", tc.name, tc.expectedLength, len(set)) 1287 } 1288 var not string 1289 if !tc.expectedHas { 1290 not = "not " 1291 } 1292 has := set.Has(tc.state) 1293 if has != tc.expectedHas { 1294 t.Errorf("%s: expected set to %scontain %v", tc.name, not, tc.state) 1295 } 1296 }) 1297 } 1298 } 1299 1300 func TestStatesMatch(t *testing.T) { 1301 modified := BugzillaBugState{Status: "MODIFIED"} 1302 errata := BugzillaBugState{Status: "CLOSED", Resolution: "ERRATA"} 1303 wontfix := BugzillaBugState{Status: "CLOSED", Resolution: "WONTFIX"} 1304 testCases := []struct { 1305 name string 1306 first, second []BugzillaBugState 1307 expected bool 1308 }{ 1309 { 1310 name: "empty slices match", 1311 expected: true, 1312 }, 1313 { 1314 name: "one empty, one non-empty do not match", 1315 first: []BugzillaBugState{modified}, 1316 }, 1317 { 1318 name: "identical slices match", 1319 first: []BugzillaBugState{modified}, 1320 second: []BugzillaBugState{modified}, 1321 expected: true, 1322 }, 1323 { 1324 name: "ordering does not matter", 1325 first: []BugzillaBugState{modified, errata}, 1326 second: []BugzillaBugState{errata, modified}, 1327 expected: true, 1328 }, 1329 { 1330 name: "different slices do not match", 1331 first: []BugzillaBugState{modified, errata}, 1332 second: []BugzillaBugState{modified, wontfix}, 1333 expected: false, 1334 }, 1335 { 1336 name: "suffix in first operand is not ignored", 1337 first: []BugzillaBugState{modified, errata}, 1338 second: []BugzillaBugState{modified}, 1339 expected: false, 1340 }, 1341 { 1342 name: "suffix in second operand is not ignored", 1343 first: []BugzillaBugState{modified}, 1344 second: []BugzillaBugState{modified, errata}, 1345 expected: false, 1346 }, 1347 } 1348 1349 for _, tc := range testCases { 1350 t.Run(tc.name, func(t *testing.T) { 1351 actual := statesMatch(tc.first, tc.second) 1352 if actual != tc.expected { 1353 t.Errorf("%s: expected %t, got %t", tc.name, tc.expected, actual) 1354 } 1355 }) 1356 } 1357 } 1358 1359 func TestValidateConfigUpdater(t *testing.T) { 1360 testCases := []struct { 1361 name string 1362 cu *ConfigUpdater 1363 expected error 1364 expectedMsg string 1365 }{ 1366 { 1367 name: "same key of different cms in different ns", 1368 cu: &ConfigUpdater{ 1369 Maps: map[string]ConfigMapSpec{ 1370 "core-services/prow/02_config/_plugins.yaml": { 1371 Name: "plugins", 1372 Key: "plugins.yaml", 1373 Clusters: map[string][]string{"first": {"some-namespace"}}, 1374 }, 1375 "somewhere/else/plugins.yaml": { 1376 Name: "plugins", 1377 Key: "plugins.yaml", 1378 Clusters: map[string][]string{"first": {"other-namespace"}}, 1379 }, 1380 }, 1381 }, 1382 expected: nil, 1383 }, 1384 { 1385 name: "same key of a cm in the same ns", 1386 cu: &ConfigUpdater{ 1387 Maps: map[string]ConfigMapSpec{ 1388 "core-services/prow/02_config/_plugins.yaml": { 1389 Name: "plugins", 1390 Key: "plugins.yaml", 1391 Clusters: map[string][]string{"first": {"some-namespace"}}, 1392 }, 1393 "somewhere/else/plugins.yaml": { 1394 Name: "plugins", 1395 Key: "plugins.yaml", 1396 Clusters: map[string][]string{"first": {"some-namespace"}}, 1397 }, 1398 }, 1399 }, 1400 expected: fmt.Errorf("key plugins.yaml in configmap plugins updated with more than one file"), 1401 }, 1402 { 1403 name: "same key of a cm in the same ns different clusters", 1404 cu: &ConfigUpdater{ 1405 Maps: map[string]ConfigMapSpec{ 1406 "core-services/prow/02_config/_plugins.yaml": { 1407 Name: "plugins", 1408 Key: "plugins.yaml", 1409 Clusters: map[string][]string{"first": {"some-namespace"}}, 1410 }, 1411 "somewhere/else/plugins.yaml": { 1412 Name: "plugins", 1413 Key: "plugins.yaml", 1414 Clusters: map[string][]string{"other": {"some-namespace"}}, 1415 }, 1416 }, 1417 }, 1418 expected: nil, 1419 }, 1420 } 1421 1422 for _, tc := range testCases { 1423 t.Run(tc.name, func(t *testing.T) { 1424 actual := validateConfigUpdater(tc.cu) 1425 if tc.expected == nil && actual != nil { 1426 t.Errorf("unexpected error: '%v'", actual) 1427 } 1428 if tc.expected != nil && actual == nil { 1429 t.Errorf("expected error '%v'', but it is nil", tc.expected) 1430 } 1431 if tc.expected != nil && actual != nil && tc.expected.Error() != actual.Error() { 1432 t.Errorf("expected error '%v', but it is '%v'", tc.expected, actual) 1433 } 1434 }) 1435 } 1436 } 1437 1438 func TestConfigUpdaterResolve(t *testing.T) { 1439 testCases := []struct { 1440 name string 1441 in ConfigUpdater 1442 expectedConfig ConfigUpdater 1443 exppectedError string 1444 }{ 1445 { 1446 name: "both cluster and cluster_groups is set, error", 1447 in: ConfigUpdater{Maps: map[string]ConfigMapSpec{"map": {Clusters: map[string][]string{"cluster": nil}, ClusterGroups: []string{"group"}}}}, 1448 exppectedError: "item maps.map contains both clusters and cluster_groups", 1449 }, 1450 { 1451 name: "inexistent cluster_group is referenced, error", 1452 in: ConfigUpdater{Maps: map[string]ConfigMapSpec{"map": {ClusterGroups: []string{"group"}}}}, 1453 exppectedError: "item maps.map.cluster_groups.0 references inexistent cluster group named group", 1454 }, 1455 { 1456 name: "successful resolving", 1457 in: ConfigUpdater{ 1458 ClusterGroups: map[string]ClusterGroup{ 1459 "some-group": {Clusters: []string{"cluster-a"}, Namespaces: []string{"namespace-a"}}, 1460 "another-group": {Clusters: []string{"cluster-b"}, Namespaces: []string{"namespace-b"}}, 1461 }, 1462 Maps: map[string]ConfigMapSpec{"map": { 1463 Name: "name", 1464 Key: "key", 1465 GZIP: utilpointer.Bool(true), 1466 ClusterGroups: []string{"some-group", "another-group"}}, 1467 }, 1468 }, 1469 expectedConfig: ConfigUpdater{ 1470 Maps: map[string]ConfigMapSpec{"map": { 1471 Name: "name", 1472 Key: "key", 1473 GZIP: utilpointer.Bool(true), 1474 Clusters: map[string][]string{ 1475 "cluster-a": {"namespace-a"}, 1476 "cluster-b": {"namespace-b"}, 1477 }}}, 1478 }, 1479 }, 1480 } 1481 1482 for _, tc := range testCases { 1483 t.Run(tc.name, func(t *testing.T) { 1484 1485 var errMsg string 1486 err := tc.in.resolve() 1487 if err != nil { 1488 errMsg = err.Error() 1489 } 1490 if errMsg != tc.exppectedError { 1491 t.Fatalf("expected error %s, got error %s", tc.exppectedError, errMsg) 1492 } 1493 if err != nil { 1494 return 1495 } 1496 1497 if diff := cmp.Diff(tc.expectedConfig, tc.in); diff != "" { 1498 t.Errorf("expected config differs from actual config: %s", diff) 1499 } 1500 }) 1501 } 1502 } 1503 1504 func TestEnabledReposForPlugin(t *testing.T) { 1505 pluginsYaml := []byte(` 1506 orgA: 1507 excluded_repos: 1508 - repoB 1509 plugins: 1510 - pluginCommon 1511 - pluginNotForRepoB 1512 orgA/repoB: 1513 plugins: 1514 - pluginCommon 1515 - pluginOnlyForRepoB 1516 `) 1517 var p Plugins 1518 err := yaml.Unmarshal(pluginsYaml, &p) 1519 if err != nil { 1520 t.Errorf("cannot unmarshal plugins config: %v", err) 1521 } 1522 cfg := Configuration{ 1523 Plugins: p, 1524 } 1525 testCases := []struct { 1526 name string 1527 wantOrgs []string 1528 wantRepos []string 1529 wantExcludedRepos map[string]sets.Set[string] 1530 }{ 1531 { 1532 name: "pluginCommon", 1533 wantOrgs: []string{"orgA"}, 1534 wantRepos: []string{"orgA/repoB"}, 1535 wantExcludedRepos: map[string]sets.Set[string]{"orgA": {}}, 1536 }, 1537 { 1538 name: "pluginNotForRepoB", 1539 wantOrgs: []string{"orgA"}, 1540 wantRepos: nil, 1541 wantExcludedRepos: map[string]sets.Set[string]{"orgA": {"orgA/repoB": {}}}, 1542 }, 1543 { 1544 name: "pluginOnlyForRepoB", 1545 wantOrgs: nil, 1546 wantRepos: []string{"orgA/repoB"}, 1547 wantExcludedRepos: map[string]sets.Set[string]{}, 1548 }, 1549 } 1550 for _, tc := range testCases { 1551 t.Run(tc.name, func(t *testing.T) { 1552 orgs, repos, excludedRepos := cfg.EnabledReposForPlugin(tc.name) 1553 if diff := cmp.Diff(tc.wantOrgs, orgs); diff != "" { 1554 t.Errorf("expected wantOrgs differ from actual: %s", diff) 1555 } 1556 if diff := cmp.Diff(tc.wantRepos, repos); diff != "" { 1557 t.Errorf("expected repos differ from actual: %s", diff) 1558 } 1559 if diff := cmp.Diff(tc.wantExcludedRepos, excludedRepos); diff != "" { 1560 t.Errorf("expected excludedRepos differ from actual: %s", diff) 1561 } 1562 }) 1563 } 1564 } 1565 1566 func TestPluginsUnmarshalFailed(t *testing.T) { 1567 badPluginsYaml := []byte(` 1568 orgA: 1569 excluded_repos = [ repoB ] 1570 plugins: 1571 - pluginCommon 1572 - pluginNotForRepoB 1573 orgA/repoB: 1574 plugins: 1575 - pluginCommon 1576 - pluginOnlyForRepoB 1577 `) 1578 var p Plugins 1579 err := p.UnmarshalJSON(badPluginsYaml) 1580 if err == nil { 1581 t.Error("expected unmarshal error but didn't get one") 1582 } 1583 } 1584 1585 func TestConfigMergingProperties(t *testing.T) { 1586 t.Parallel() 1587 testCases := []struct { 1588 name string 1589 makeMergeable func(*Configuration) 1590 }{ 1591 { 1592 name: "Plugins config", 1593 makeMergeable: func(c *Configuration) { 1594 *c = Configuration{Plugins: c.Plugins, Bugzilla: c.Bugzilla} 1595 }, 1596 }, 1597 } 1598 1599 expectedProperties := []struct { 1600 name string 1601 verification func(t *testing.T, fuzzedConfig *Configuration) 1602 }{ 1603 { 1604 name: "Merging into empty config always succeeds and makes the empty config equal to the one that was merged in", 1605 verification: func(t *testing.T, fuzzedMergeableConfig *Configuration) { 1606 newConfig := &Configuration{} 1607 if err := newConfig.mergeFrom(fuzzedMergeableConfig); err != nil { 1608 t.Fatalf("merging fuzzed mergeable config into empty config failed: %v", err) 1609 } 1610 if diff := cmp.Diff(newConfig, fuzzedMergeableConfig); diff != "" { 1611 t.Errorf("after merging config into an empty config, the config that was merged into differs from the one we merged from:\n%s\n", diff) 1612 } 1613 }, 1614 }, 1615 { 1616 name: "Merging empty config in always succeeds", 1617 verification: func(t *testing.T, fuzzedMergeableConfig *Configuration) { 1618 if err := fuzzedMergeableConfig.mergeFrom(&Configuration{}); err != nil { 1619 t.Errorf("merging empty config in failed: %v", err) 1620 } 1621 }, 1622 }, 1623 { 1624 name: "Merging a config into itself always fails", 1625 verification: func(t *testing.T, fuzzedMergeableConfig *Configuration) { 1626 1627 // An empty bugzilla org config does nothing, so clean those. 1628 for org, val := range fuzzedMergeableConfig.Bugzilla.Orgs { 1629 if reflect.DeepEqual(val, BugzillaOrgOptions{}) { 1630 delete(fuzzedMergeableConfig.Bugzilla.Orgs, org) 1631 } 1632 } 1633 // An exception to the rule is merging an empty config into itself, that is valid and will just do nothing. 1634 if apiequality.Semantic.DeepEqual(fuzzedMergeableConfig, &Configuration{}) { 1635 return 1636 } 1637 1638 if err := fuzzedMergeableConfig.mergeFrom(fuzzedMergeableConfig); err == nil { 1639 serialized, serializeErr := yaml.Marshal(fuzzedMergeableConfig) 1640 if serializeErr != nil { 1641 t.Fatalf("merging non-empty config into itself did not yield an error and serializing it afterwards failed: %v. Raw object: %+v", serializeErr, fuzzedMergeableConfig) 1642 } 1643 t.Errorf("merging a config into itself did not produce an error. Serialized config:\n%s", string(serialized)) 1644 } 1645 }, 1646 }, 1647 } 1648 1649 seed := time.Now().UnixNano() 1650 // Print the seed so failures can easily be reproduced 1651 t.Logf("Seed: %d", seed) 1652 fuzzer := fuzz.NewWithSeed(seed) 1653 1654 for _, tc := range testCases { 1655 tc := tc 1656 t.Run(tc.name, func(t *testing.T) { 1657 t.Parallel() 1658 1659 for _, propertyTest := range expectedProperties { 1660 propertyTest := propertyTest 1661 t.Run(propertyTest.name, func(t *testing.T) { 1662 t.Parallel() 1663 1664 for i := 0; i < 100; i++ { 1665 fuzzedConfig := &Configuration{} 1666 fuzzer.Fuzz(fuzzedConfig) 1667 1668 tc.makeMergeable(fuzzedConfig) 1669 1670 propertyTest.verification(t, fuzzedConfig) 1671 } 1672 }) 1673 } 1674 }) 1675 } 1676 } 1677 1678 func TestPluginsMergeFrom(t *testing.T) { 1679 t.Parallel() 1680 testCases := []struct { 1681 name string 1682 1683 from *Plugins 1684 to *Plugins 1685 1686 expected *Plugins 1687 expectedErrMsg string 1688 }{ 1689 { 1690 name: "Merging for two different repos succeeds", 1691 1692 from: &Plugins{"org/repo-1": OrgPlugins{Plugins: []string{"wip"}}}, 1693 to: &Plugins{"org/repo-2": OrgPlugins{Plugins: []string{"wip"}}}, 1694 1695 expected: &Plugins{ 1696 "org/repo-1": OrgPlugins{Plugins: []string{"wip"}}, 1697 "org/repo-2": OrgPlugins{Plugins: []string{"wip"}}, 1698 }, 1699 }, 1700 { 1701 name: "Merging the same repo fails", 1702 1703 from: &Plugins{"org/repo-1": OrgPlugins{Plugins: []string{"wip"}}}, 1704 to: &Plugins{"org/repo-1": OrgPlugins{Plugins: []string{"wip"}}}, 1705 1706 expectedErrMsg: "found duplicate config for plugins.org/repo-1", 1707 }, 1708 } 1709 1710 for _, tc := range testCases { 1711 t.Run(tc.name, func(t *testing.T) { 1712 var errMsg string 1713 err := tc.to.mergeFrom(tc.from) 1714 if err != nil { 1715 errMsg = err.Error() 1716 } 1717 if tc.expectedErrMsg != errMsg { 1718 t.Fatalf("expected error message %q, got %s", tc.expectedErrMsg, errMsg) 1719 } 1720 if err != nil { 1721 return 1722 } 1723 1724 if diff := cmp.Diff(tc.expected, tc.to); diff != "" { 1725 t.Errorf("expexcted config differs from actual: %s", diff) 1726 } 1727 }) 1728 } 1729 } 1730 1731 func TestBugzillaMergeFrom(t *testing.T) { 1732 t.Parallel() 1733 1734 yes := true 1735 targetRelease1 := "target-release-1" 1736 targetRelease2 := "target-release-2" 1737 1738 testCases := []struct { 1739 name string 1740 1741 from *Bugzilla 1742 to *Bugzilla 1743 1744 expected *Bugzilla 1745 expectedErrMsg string 1746 }{ 1747 { 1748 name: "Merging for two different repos", 1749 1750 from: &Bugzilla{Orgs: map[string]BugzillaOrgOptions{ 1751 "org": { 1752 Repos: map[string]BugzillaRepoOptions{ 1753 "repo-1": { 1754 Branches: map[string]BugzillaBranchOptions{ 1755 "master": { 1756 IsOpen: &yes, 1757 TargetRelease: &targetRelease1, 1758 }, 1759 }, 1760 }, 1761 }, 1762 }, 1763 }}, 1764 to: &Bugzilla{Orgs: map[string]BugzillaOrgOptions{ 1765 "org": { 1766 Repos: map[string]BugzillaRepoOptions{ 1767 "repo-2": { 1768 Branches: map[string]BugzillaBranchOptions{ 1769 "master": { 1770 IsOpen: &yes, 1771 TargetRelease: &targetRelease2, 1772 }, 1773 }, 1774 }, 1775 }, 1776 }, 1777 }}, 1778 1779 expected: &Bugzilla{Orgs: map[string]BugzillaOrgOptions{ 1780 "org": { 1781 Repos: map[string]BugzillaRepoOptions{ 1782 "repo-1": { 1783 Branches: map[string]BugzillaBranchOptions{ 1784 "master": { 1785 IsOpen: &yes, 1786 TargetRelease: &targetRelease1, 1787 }, 1788 }, 1789 }, 1790 "repo-2": { 1791 Branches: map[string]BugzillaBranchOptions{ 1792 "master": { 1793 IsOpen: &yes, 1794 TargetRelease: &targetRelease2, 1795 }, 1796 }, 1797 }, 1798 }, 1799 }, 1800 }}, 1801 }, 1802 { 1803 name: "Merging organization defaults and repo in org", 1804 1805 from: &Bugzilla{Orgs: map[string]BugzillaOrgOptions{ 1806 "org": { 1807 Repos: map[string]BugzillaRepoOptions{ 1808 "repo-2": { 1809 Branches: map[string]BugzillaBranchOptions{ 1810 "master": { 1811 IsOpen: &yes, 1812 TargetRelease: &targetRelease2, 1813 }, 1814 }, 1815 }, 1816 }, 1817 }, 1818 }}, 1819 to: &Bugzilla{Orgs: map[string]BugzillaOrgOptions{ 1820 "org": { 1821 Default: map[string]BugzillaBranchOptions{ 1822 "master": { 1823 IsOpen: &yes, 1824 TargetRelease: &targetRelease1, 1825 }, 1826 }, 1827 }, 1828 }}, 1829 1830 expected: &Bugzilla{Orgs: map[string]BugzillaOrgOptions{ 1831 "org": { 1832 Default: map[string]BugzillaBranchOptions{ 1833 "master": { 1834 IsOpen: &yes, 1835 TargetRelease: &targetRelease1, 1836 }, 1837 }, 1838 Repos: map[string]BugzillaRepoOptions{ 1839 "repo-2": { 1840 Branches: map[string]BugzillaBranchOptions{ 1841 "master": { 1842 IsOpen: &yes, 1843 TargetRelease: &targetRelease2, 1844 }, 1845 }, 1846 }, 1847 }, 1848 }, 1849 }}, 1850 }, 1851 { 1852 name: "Merging 2 organizations", 1853 1854 from: &Bugzilla{Orgs: map[string]BugzillaOrgOptions{ 1855 "org": { 1856 Repos: map[string]BugzillaRepoOptions{ 1857 "repo-1": { 1858 Branches: map[string]BugzillaBranchOptions{ 1859 "master": { 1860 IsOpen: &yes, 1861 TargetRelease: &targetRelease1, 1862 }, 1863 }, 1864 }, 1865 }, 1866 }, 1867 }}, 1868 to: &Bugzilla{Orgs: map[string]BugzillaOrgOptions{ 1869 "org-2": { 1870 Repos: map[string]BugzillaRepoOptions{ 1871 "repo-1": { 1872 Branches: map[string]BugzillaBranchOptions{ 1873 "master": { 1874 IsOpen: &yes, 1875 TargetRelease: &targetRelease2, 1876 }, 1877 }, 1878 }, 1879 }, 1880 }, 1881 }}, 1882 1883 expected: &Bugzilla{Orgs: map[string]BugzillaOrgOptions{ 1884 "org": { 1885 Repos: map[string]BugzillaRepoOptions{ 1886 "repo-1": { 1887 Branches: map[string]BugzillaBranchOptions{ 1888 "master": { 1889 IsOpen: &yes, 1890 TargetRelease: &targetRelease1, 1891 }, 1892 }, 1893 }, 1894 }}, 1895 "org-2": { 1896 Repos: map[string]BugzillaRepoOptions{ 1897 "repo-1": { 1898 Branches: map[string]BugzillaBranchOptions{ 1899 "master": { 1900 IsOpen: &yes, 1901 TargetRelease: &targetRelease2, 1902 }, 1903 }, 1904 }, 1905 }, 1906 }, 1907 }}, 1908 }, 1909 { 1910 name: "Merging global defaults succeeds", 1911 1912 from: &Bugzilla{Default: map[string]BugzillaBranchOptions{ 1913 "master": { 1914 IsOpen: &yes, 1915 TargetRelease: &targetRelease1, 1916 }, 1917 }}, 1918 to: &Bugzilla{Orgs: map[string]BugzillaOrgOptions{ 1919 "org": { 1920 Repos: map[string]BugzillaRepoOptions{ 1921 "repo-1": { 1922 Branches: map[string]BugzillaBranchOptions{ 1923 "master": { 1924 IsOpen: &yes, 1925 TargetRelease: &targetRelease1, 1926 }, 1927 }, 1928 }, 1929 }, 1930 }, 1931 }}, 1932 expected: &Bugzilla{Default: map[string]BugzillaBranchOptions{ 1933 "master": { 1934 IsOpen: &yes, 1935 TargetRelease: &targetRelease1, 1936 }, 1937 }, Orgs: map[string]BugzillaOrgOptions{ 1938 "org": { 1939 Repos: map[string]BugzillaRepoOptions{ 1940 "repo-1": { 1941 Branches: map[string]BugzillaBranchOptions{ 1942 "master": { 1943 IsOpen: &yes, 1944 TargetRelease: &targetRelease1, 1945 }, 1946 }, 1947 }, 1948 }, 1949 }, 1950 }}, 1951 }, 1952 { 1953 name: "Merging multiple global defaults fails", 1954 1955 from: &Bugzilla{Default: map[string]BugzillaBranchOptions{ 1956 "master": { 1957 IsOpen: &yes, 1958 TargetRelease: &targetRelease1, 1959 }, 1960 }}, 1961 to: &Bugzilla{Default: map[string]BugzillaBranchOptions{ 1962 "master": { 1963 IsOpen: &yes, 1964 TargetRelease: &targetRelease2, 1965 }, 1966 }}, 1967 expectedErrMsg: "configuration of global default defined in multiple places", 1968 }, 1969 { 1970 name: "Merging same organization defaults fails", 1971 1972 from: &Bugzilla{Orgs: map[string]BugzillaOrgOptions{ 1973 "org": { 1974 Default: map[string]BugzillaBranchOptions{ 1975 "master": { 1976 IsOpen: &yes, 1977 TargetRelease: &targetRelease1, 1978 }, 1979 }, 1980 }, 1981 }}, 1982 to: &Bugzilla{Orgs: map[string]BugzillaOrgOptions{ 1983 "org": { 1984 Default: map[string]BugzillaBranchOptions{ 1985 "master": { 1986 IsOpen: &yes, 1987 TargetRelease: &targetRelease2, 1988 }, 1989 }, 1990 }, 1991 }}, 1992 1993 expectedErrMsg: "found duplicate organization config for bugzilla.org", 1994 }, 1995 { 1996 name: "Merging same repository fails", 1997 1998 from: &Bugzilla{Orgs: map[string]BugzillaOrgOptions{ 1999 "org": { 2000 Repos: map[string]BugzillaRepoOptions{ 2001 "repo-1": { 2002 Branches: map[string]BugzillaBranchOptions{ 2003 "master": { 2004 IsOpen: &yes, 2005 TargetRelease: &targetRelease1, 2006 }, 2007 }, 2008 }, 2009 }, 2010 }, 2011 }}, 2012 to: &Bugzilla{Orgs: map[string]BugzillaOrgOptions{ 2013 "org": { 2014 Repos: map[string]BugzillaRepoOptions{ 2015 "repo-1": { 2016 Branches: map[string]BugzillaBranchOptions{ 2017 "master": { 2018 IsOpen: &yes, 2019 TargetRelease: &targetRelease2, 2020 }, 2021 }, 2022 }, 2023 }, 2024 }, 2025 }}, 2026 2027 expectedErrMsg: "found duplicate repository config for bugzilla.org/repo-1", 2028 }, 2029 } 2030 2031 for _, tc := range testCases { 2032 t.Run(tc.name, func(t *testing.T) { 2033 var errMsg string 2034 err := tc.to.mergeFrom(tc.from) 2035 if err != nil { 2036 errMsg = err.Error() 2037 } 2038 if tc.expectedErrMsg != errMsg { 2039 t.Fatalf("expected error message %q, got %q", tc.expectedErrMsg, errMsg) 2040 } 2041 if err != nil { 2042 return 2043 } 2044 2045 if diff := cmp.Diff(tc.expected, tc.to); diff != "" { 2046 t.Errorf("expexcted config differs from actual: %s", diff) 2047 } 2048 }) 2049 } 2050 } 2051 2052 func TestHasConfigFor(t *testing.T) { 2053 t.Parallel() 2054 testCases := []struct { 2055 name string 2056 resultGenerator func(fuzzedConfig *Configuration) (toCheck *Configuration, expectGlobal bool, expectOrgs sets.Set[string], expectRepos sets.Set[string]) 2057 }{ 2058 { 2059 name: "Any non-empty config with empty Plugins and Bugzilla is considered to be global", 2060 resultGenerator: func(fuzzedConfig *Configuration) (toCheck *Configuration, expectGlobal bool, expectOrgs sets.Set[string], expectRepos sets.Set[string]) { 2061 fuzzedConfig.Plugins = nil 2062 fuzzedConfig.Bugzilla = Bugzilla{} 2063 fuzzedConfig.Approve = nil 2064 fuzzedConfig.Label.RestrictedLabels = nil 2065 fuzzedConfig.Lgtm = nil 2066 fuzzedConfig.Triggers = nil 2067 fuzzedConfig.Welcome = nil 2068 fuzzedConfig.ExternalPlugins = nil 2069 return fuzzedConfig, !reflect.DeepEqual(fuzzedConfig, &Configuration{}), nil, nil 2070 }, 2071 }, 2072 { 2073 name: "Any config with plugins is considered to be for the orgs and repos references there", 2074 resultGenerator: func(fuzzedConfig *Configuration) (toCheck *Configuration, expectGlobal bool, expectOrgs sets.Set[string], expectRepos sets.Set[string]) { 2075 // exclude non-plugins configs to test plugins specifically 2076 fuzzedConfig = &Configuration{Plugins: fuzzedConfig.Plugins} 2077 expectOrgs, expectRepos = sets.Set[string]{}, sets.Set[string]{} 2078 for orgOrRepo := range fuzzedConfig.Plugins { 2079 if strings.Contains(orgOrRepo, "/") { 2080 expectRepos.Insert(orgOrRepo) 2081 } else { 2082 expectOrgs.Insert(orgOrRepo) 2083 } 2084 } 2085 return fuzzedConfig, !reflect.DeepEqual(fuzzedConfig, &Configuration{Plugins: fuzzedConfig.Plugins}), expectOrgs, expectRepos 2086 }, 2087 }, 2088 { 2089 name: "Any config with bugzilla is considered to be for the orgs and repos references there", 2090 resultGenerator: func(fuzzedConfig *Configuration) (toCheck *Configuration, expectGlobal bool, expectOrgs sets.Set[string], expectRepos sets.Set[string]) { 2091 // exclude non-plugins configs to test bugzilla specifically 2092 fuzzedConfig = &Configuration{Bugzilla: fuzzedConfig.Bugzilla} 2093 expectOrgs, expectRepos = sets.Set[string]{}, sets.Set[string]{} 2094 for org, orgConfig := range fuzzedConfig.Bugzilla.Orgs { 2095 if orgConfig.Default != nil { 2096 expectOrgs.Insert(org) 2097 } 2098 for repo := range orgConfig.Repos { 2099 expectRepos.Insert(org + "/" + repo) 2100 } 2101 } 2102 return fuzzedConfig, len(fuzzedConfig.Bugzilla.Default) > 0, expectOrgs, expectRepos 2103 }, 2104 }, 2105 { 2106 name: "Any config with approve is considered to be for the orgs and repos references there", 2107 resultGenerator: func(fuzzedConfig *Configuration) (toCheck *Configuration, expectGlobal bool, expectOrgs sets.Set[string], expectRepos sets.Set[string]) { 2108 fuzzedConfig = &Configuration{Approve: fuzzedConfig.Approve} 2109 expectOrgs, expectRepos = sets.Set[string]{}, sets.Set[string]{} 2110 2111 for _, approveConfig := range fuzzedConfig.Approve { 2112 for _, orgOrRepo := range approveConfig.Repos { 2113 if strings.Contains(orgOrRepo, "/") { 2114 expectRepos.Insert(orgOrRepo) 2115 } else { 2116 expectOrgs.Insert(orgOrRepo) 2117 } 2118 } 2119 } 2120 2121 return fuzzedConfig, false, expectOrgs, expectRepos 2122 }, 2123 }, 2124 { 2125 name: "Any config with lgtm is considered to be for the orgs and repos references there", 2126 resultGenerator: func(fuzzedConfig *Configuration) (toCheck *Configuration, expectGlobal bool, expectOrgs sets.Set[string], expectRepos sets.Set[string]) { 2127 fuzzedConfig = &Configuration{Lgtm: fuzzedConfig.Lgtm} 2128 expectOrgs, expectRepos = sets.Set[string]{}, sets.Set[string]{} 2129 2130 for _, lgtm := range fuzzedConfig.Lgtm { 2131 for _, orgOrRepo := range lgtm.Repos { 2132 if strings.Contains(orgOrRepo, "/") { 2133 expectRepos.Insert(orgOrRepo) 2134 } else { 2135 expectOrgs.Insert(orgOrRepo) 2136 } 2137 } 2138 } 2139 2140 return fuzzedConfig, false, expectOrgs, expectRepos 2141 }, 2142 }, 2143 { 2144 name: "Any config with triggers is considered to be for the orgs and repos references there", 2145 resultGenerator: func(fuzzedConfig *Configuration) (toCheck *Configuration, expectGlobal bool, expectOrgs sets.Set[string], expectRepos sets.Set[string]) { 2146 fuzzedConfig = &Configuration{Triggers: fuzzedConfig.Triggers} 2147 expectOrgs, expectRepos = sets.Set[string]{}, sets.Set[string]{} 2148 2149 for _, trigger := range fuzzedConfig.Triggers { 2150 for _, orgOrRepo := range trigger.Repos { 2151 if strings.Contains(orgOrRepo, "/") { 2152 expectRepos.Insert(orgOrRepo) 2153 } else { 2154 expectOrgs.Insert(orgOrRepo) 2155 } 2156 } 2157 } 2158 2159 return fuzzedConfig, false, expectOrgs, expectRepos 2160 }, 2161 }, 2162 { 2163 name: "Any config with welcome is considered to be for the orgs and repos references there", 2164 resultGenerator: func(fuzzedConfig *Configuration) (toCheck *Configuration, expectGlobal bool, expectOrgs sets.Set[string], expectRepos sets.Set[string]) { 2165 fuzzedConfig = &Configuration{Welcome: fuzzedConfig.Welcome} 2166 expectOrgs, expectRepos = sets.Set[string]{}, sets.Set[string]{} 2167 2168 for _, welcome := range fuzzedConfig.Welcome { 2169 for _, orgOrRepo := range welcome.Repos { 2170 if strings.Contains(orgOrRepo, "/") { 2171 expectRepos.Insert(orgOrRepo) 2172 } else { 2173 expectOrgs.Insert(orgOrRepo) 2174 } 2175 } 2176 } 2177 2178 return fuzzedConfig, false, expectOrgs, expectRepos 2179 }, 2180 }, 2181 { 2182 name: "Any config with external-plugins is considered to be for the orgs and repos references there", 2183 resultGenerator: func(fuzzedConfig *Configuration) (toCheck *Configuration, expectGlobal bool, expectOrgs sets.Set[string], expectRepos sets.Set[string]) { 2184 fuzzedConfig = &Configuration{ExternalPlugins: fuzzedConfig.ExternalPlugins} 2185 expectOrgs, expectRepos = sets.Set[string]{}, sets.Set[string]{} 2186 2187 for orgOrRepo := range fuzzedConfig.ExternalPlugins { 2188 if strings.Contains(orgOrRepo, "/") { 2189 expectRepos.Insert(orgOrRepo) 2190 } else { 2191 expectOrgs.Insert(orgOrRepo) 2192 } 2193 } 2194 return fuzzedConfig, false, expectOrgs, expectRepos 2195 }, 2196 }, 2197 { 2198 name: "Any config with label.restricted_labels is considered to be for the org and repos references there", 2199 resultGenerator: func(fuzzedConfig *Configuration) (toCheck *Configuration, expectGlobal bool, expectOrgs sets.Set[string], expectRepos sets.Set[string]) { 2200 fuzzedConfig = &Configuration{Label: fuzzedConfig.Label} 2201 if len(fuzzedConfig.Label.AdditionalLabels) > 0 { 2202 expectGlobal = true 2203 } 2204 2205 expectOrgs, expectRepos = sets.Set[string]{}, sets.Set[string]{} 2206 2207 for orgOrRepo := range fuzzedConfig.Label.RestrictedLabels { 2208 if orgOrRepo == "*" { 2209 expectGlobal = true 2210 } else if strings.Contains(orgOrRepo, "/") { 2211 expectRepos.Insert(orgOrRepo) 2212 } else { 2213 expectOrgs.Insert(orgOrRepo) 2214 } 2215 } 2216 return fuzzedConfig, expectGlobal, expectOrgs, expectRepos 2217 }, 2218 }, 2219 } 2220 2221 seed := time.Now().UnixNano() 2222 // Print the seed so failures can easily be reproduced 2223 t.Logf("Seed: %d", seed) 2224 fuzzer := fuzz.NewWithSeed(seed) 2225 2226 for _, tc := range testCases { 2227 t.Run(tc.name, func(t *testing.T) { 2228 for i := 0; i < 100; i++ { 2229 fuzzedConfig := &Configuration{} 2230 fuzzer.Fuzz(fuzzedConfig) 2231 2232 fuzzedAndManipulatedConfig, expectIsGlobal, expectOrgs, expectRepos := tc.resultGenerator(fuzzedConfig) 2233 actualIsGlobal, actualOrgs, actualRepos := fuzzedAndManipulatedConfig.HasConfigFor() 2234 2235 if expectIsGlobal != actualIsGlobal { 2236 t.Errorf("exepcted isGlobal: %t, got: %t", expectIsGlobal, actualIsGlobal) 2237 } 2238 2239 if diff := cmp.Diff(expectOrgs, actualOrgs); diff != "" { 2240 t.Errorf("expected orgs differ from actual: %s", diff) 2241 } 2242 2243 if diff := cmp.Diff(expectRepos, actualRepos); diff != "" { 2244 t.Errorf("expected repos differ from actual: %s", diff) 2245 } 2246 } 2247 }) 2248 } 2249 } 2250 2251 func TestMergeFrom(t *testing.T) { 2252 t.Parallel() 2253 testCases := []struct { 2254 name string 2255 in Configuration 2256 supplementalConfigs []Configuration 2257 expected Configuration 2258 errorExpected bool 2259 }{ 2260 { 2261 name: "Approve config gets merged", 2262 in: Configuration{Approve: []Approve{{Repos: []string{"foo/bar"}}}}, 2263 supplementalConfigs: []Configuration{{Approve: []Approve{{Repos: []string{"foo/baz"}}}}}, 2264 expected: Configuration{Approve: []Approve{ 2265 {Repos: []string{"foo/bar"}}, 2266 {Repos: []string{"foo/baz"}}, 2267 }}, 2268 }, 2269 { 2270 name: "LGTM config gets merged", 2271 in: Configuration{Lgtm: []Lgtm{{Repos: []string{"foo/bar"}}}}, 2272 supplementalConfigs: []Configuration{{Lgtm: []Lgtm{{Repos: []string{"foo/baz"}}}}}, 2273 expected: Configuration{Lgtm: []Lgtm{ 2274 {Repos: []string{"foo/bar"}}, 2275 {Repos: []string{"foo/baz"}}, 2276 }}, 2277 }, 2278 { 2279 name: "Triggers config gets merged", 2280 in: Configuration{Triggers: []Trigger{{Repos: []string{"foo/bar"}}}}, 2281 supplementalConfigs: []Configuration{{Triggers: []Trigger{{Repos: []string{"foo/baz"}}}}}, 2282 expected: Configuration{Triggers: []Trigger{ 2283 {Repos: []string{"foo/bar"}}, 2284 {Repos: []string{"foo/baz"}}, 2285 }}, 2286 }, 2287 { 2288 name: "Welcome config gets merged", 2289 in: Configuration{Welcome: []Welcome{{Repos: []string{"foo/bar"}}}}, 2290 supplementalConfigs: []Configuration{{Welcome: []Welcome{{Repos: []string{"foo/baz"}}}}}, 2291 expected: Configuration{Welcome: []Welcome{ 2292 {Repos: []string{"foo/bar"}}, 2293 {Repos: []string{"foo/baz"}}, 2294 }}, 2295 }, 2296 { 2297 name: "ExternalPlugins get merged", 2298 in: Configuration{ 2299 ExternalPlugins: map[string][]ExternalPlugin{ 2300 "foo/bar": {{Name: "refresh", Endpoint: "http://refresh", Events: []string{"issue_comment"}}}, 2301 }, 2302 }, 2303 supplementalConfigs: []Configuration{{ExternalPlugins: map[string][]ExternalPlugin{"foo/baz": {{Name: "refresh", Endpoint: "http://refresh", Events: []string{"issue_comment"}}}}}}, 2304 expected: Configuration{ 2305 ExternalPlugins: map[string][]ExternalPlugin{ 2306 "foo/bar": {{Name: "refresh", Endpoint: "http://refresh", Events: []string{"issue_comment"}}}, 2307 "foo/baz": {{Name: "refresh", Endpoint: "http://refresh", Events: []string{"issue_comment"}}}, 2308 }, 2309 }, 2310 }, 2311 { 2312 name: "Labels.restricted_config gets merged", 2313 in: Configuration{Label: Label{AdditionalLabels: []string{"foo"}}}, 2314 supplementalConfigs: []Configuration{{Label: Label{RestrictedLabels: map[string][]RestrictedLabel{"org": {{Label: "cherry-pick-approved", AllowedTeams: []string{"patch-managers"}}}}}}}, 2315 expected: Configuration{ 2316 Label: Label{ 2317 AdditionalLabels: []string{"foo"}, 2318 RestrictedLabels: map[string][]RestrictedLabel{"org": {{Label: "cherry-pick-approved", AllowedTeams: []string{"patch-managers"}}}}, 2319 }, 2320 }, 2321 }, 2322 { 2323 name: "main config has no ExternalPlugins config, supplemental config has, it gets merged", 2324 supplementalConfigs: []Configuration{{ExternalPlugins: map[string][]ExternalPlugin{"foo/bar": {{Name: "refresh", Endpoint: "http://refresh", Events: []string{"issue_comment"}}}}}}, 2325 expected: Configuration{ 2326 ExternalPlugins: map[string][]ExternalPlugin{ 2327 "foo/bar": {{Name: "refresh", Endpoint: "http://refresh", Events: []string{"issue_comment"}}}, 2328 }, 2329 }, 2330 }, 2331 { 2332 name: "ExternalPlugins cant't merge duplicated configs", 2333 in: Configuration{ 2334 ExternalPlugins: map[string][]ExternalPlugin{ 2335 "foo/bar": {{Name: "refresh", Endpoint: "http://refresh", Events: []string{"issue_comment"}}}, 2336 }, 2337 }, 2338 supplementalConfigs: []Configuration{{ExternalPlugins: map[string][]ExternalPlugin{"foo/bar": {{Name: "refresh", Endpoint: "http://refresh", Events: []string{"issue_comment"}}}}}}, 2339 errorExpected: true, 2340 }, 2341 } 2342 2343 for _, tc := range testCases { 2344 for idx, supplementalConfig := range tc.supplementalConfigs { 2345 err := tc.in.mergeFrom(&supplementalConfig) 2346 if err != nil && !tc.errorExpected { 2347 t.Fatalf("failed to merge supplemental config %d: %v", idx, err) 2348 } 2349 if err == nil && tc.errorExpected { 2350 t.Fatal("expected error but got nothing") 2351 } 2352 } 2353 2354 if diff := cmp.Diff(tc.expected, tc.in); !tc.errorExpected && diff != "" { 2355 t.Errorf("expected config differs from expected: %s", diff) 2356 } 2357 } 2358 }