github.com/zppinho/prow@v0.0.0-20240510014325-1738badeb017/cmd/peribolos/main_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 main 18 19 import ( 20 "errors" 21 "flag" 22 "fmt" 23 "reflect" 24 "sort" 25 "testing" 26 27 "github.com/google/go-cmp/cmp" 28 "sigs.k8s.io/prow/pkg/config/org" 29 "sigs.k8s.io/prow/pkg/flagutil" 30 "sigs.k8s.io/prow/pkg/github" 31 32 "k8s.io/apimachinery/pkg/util/sets" 33 ) 34 35 func TestOptions(t *testing.T) { 36 cases := []struct { 37 name string 38 args []string 39 expected *options 40 }{ 41 { 42 name: "missing --config", 43 args: []string{}, 44 }, 45 { 46 name: "bad --github-endpoint", 47 args: []string{"--config-path=foo", "--github-endpoint=ht!tp://:dumb"}, 48 }, 49 { 50 name: "--minAdmins too low", 51 args: []string{"--config-path=foo", "--min-admins=1"}, 52 }, 53 { 54 name: "--maximum-removal-delta too high", 55 args: []string{"--config-path=foo", "--maximum-removal-delta=1.1"}, 56 }, 57 { 58 name: "--maximum-removal-delta too low", 59 args: []string{"--config-path=foo", "--maximum-removal-delta=-0.1"}, 60 }, 61 { 62 name: "reject --dump-full-config without --dump", 63 args: []string{"--config-path=foo", "--dump-full-config"}, 64 }, 65 { 66 name: "maximal delta", 67 args: []string{"--config-path=foo", "--maximum-removal-delta=1"}, 68 expected: &options{ 69 config: "foo", 70 minAdmins: defaultMinAdmins, 71 requireSelf: true, 72 maximumDelta: 1, 73 logLevel: "info", 74 }, 75 }, 76 { 77 name: "minimal delta", 78 args: []string{"--config-path=foo", "--maximum-removal-delta=0"}, 79 expected: &options{ 80 config: "foo", 81 minAdmins: defaultMinAdmins, 82 requireSelf: true, 83 maximumDelta: 0, 84 logLevel: "info", 85 }, 86 }, 87 { 88 name: "minimal admins", 89 args: []string{"--config-path=foo", "--min-admins=2"}, 90 expected: &options{ 91 config: "foo", 92 minAdmins: 2, 93 requireSelf: true, 94 maximumDelta: defaultDelta, 95 logLevel: "info", 96 }, 97 }, 98 { 99 name: "reject dump and confirm", 100 args: []string{"--confirm", "--dump=frogger"}, 101 }, 102 { 103 name: "reject dump and config-path", 104 args: []string{"--config-path=foo", "--dump=frogger"}, 105 }, 106 { 107 name: "reject --fix-team-members without --fix-teams", 108 args: []string{"--config-path=foo", "--fix-team-members"}, 109 }, 110 { 111 name: "allow dump without config", 112 args: []string{"--dump=frogger"}, 113 expected: &options{ 114 minAdmins: defaultMinAdmins, 115 requireSelf: true, 116 maximumDelta: defaultDelta, 117 dump: "frogger", 118 logLevel: "info", 119 }, 120 }, 121 { 122 name: "minimal", 123 args: []string{"--config-path=foo"}, 124 expected: &options{ 125 config: "foo", 126 minAdmins: defaultMinAdmins, 127 requireSelf: true, 128 maximumDelta: defaultDelta, 129 logLevel: "info", 130 }, 131 }, 132 { 133 name: "full", 134 args: []string{"--config-path=foo", "--github-token-path=bar", "--github-endpoint=weird://url", "--confirm=true", "--require-self=false", "--dump=", "--fix-org", "--fix-org-members", "--fix-teams", "--fix-team-members", "--log-level=debug"}, 135 expected: &options{ 136 config: "foo", 137 confirm: true, 138 requireSelf: false, 139 minAdmins: defaultMinAdmins, 140 maximumDelta: defaultDelta, 141 fixOrg: true, 142 fixOrgMembers: true, 143 fixTeams: true, 144 fixTeamMembers: true, 145 logLevel: "debug", 146 }, 147 }, 148 } 149 150 for _, tc := range cases { 151 t.Run(tc.name, func(t *testing.T) { 152 flags := flag.NewFlagSet(tc.name, flag.ContinueOnError) 153 var actual options 154 err := actual.parseArgs(flags, tc.args) 155 actual.github = flagutil.GitHubOptions{} 156 switch { 157 case err == nil && tc.expected == nil: 158 t.Errorf("%s: failed to return an error", tc.name) 159 case err != nil && tc.expected != nil: 160 t.Errorf("%s: unexpected error: %v", tc.name, err) 161 case tc.expected != nil && !reflect.DeepEqual(*tc.expected, actual): 162 t.Errorf("%s: got incorrect options: %v", tc.name, cmp.Diff(actual, *tc.expected, cmp.AllowUnexported(options{}, flagutil.Strings{}, flagutil.GitHubOptions{}))) 163 } 164 }) 165 } 166 } 167 168 type fakeClient struct { 169 orgMembers sets.Set[string] 170 admins sets.Set[string] 171 invitees sets.Set[string] 172 members sets.Set[string] 173 removed sets.Set[string] 174 newAdmins sets.Set[string] 175 newMembers sets.Set[string] 176 } 177 178 func (c *fakeClient) BotUser() (*github.UserData, error) { 179 return &github.UserData{Login: "me"}, nil 180 } 181 182 func (c fakeClient) makeMembers(people sets.Set[string]) []github.TeamMember { 183 var ret []github.TeamMember 184 for p := range people { 185 ret = append(ret, github.TeamMember{Login: p}) 186 } 187 return ret 188 } 189 190 func (c *fakeClient) ListOrgMembers(org, role string) ([]github.TeamMember, error) { 191 switch role { 192 case github.RoleMember: 193 return c.makeMembers(c.members), nil 194 case github.RoleAdmin: 195 return c.makeMembers(c.admins), nil 196 default: 197 // RoleAll: implement when/if necessary 198 return nil, fmt.Errorf("bad role: %s", role) 199 } 200 } 201 202 func (c *fakeClient) ListOrgInvitations(org string) ([]github.OrgInvitation, error) { 203 var ret []github.OrgInvitation 204 for p := range c.invitees { 205 if p == "fail" { 206 return nil, errors.New("injected list org invitations failure") 207 } 208 ret = append(ret, github.OrgInvitation{ 209 TeamMember: github.TeamMember{ 210 Login: p, 211 }, 212 }) 213 } 214 return ret, nil 215 } 216 217 func (c *fakeClient) RemoveOrgMembership(org, user string) error { 218 if user == "fail" { 219 return errors.New("injected remove org membership failure") 220 } 221 c.removed.Insert(user) 222 c.admins.Delete(user) 223 c.members.Delete(user) 224 return nil 225 } 226 227 func (c *fakeClient) UpdateOrgMembership(org, user string, admin bool) (*github.OrgMembership, error) { 228 if user == "fail" { 229 return nil, errors.New("injected update org failure") 230 } 231 var state string 232 if c.members.Has(user) || c.admins.Has(user) { 233 state = github.StateActive 234 } else { 235 state = github.StatePending 236 } 237 var role string 238 if admin { 239 c.newAdmins.Insert(user) 240 c.admins.Insert(user) 241 role = github.RoleAdmin 242 } else { 243 c.newMembers.Insert(user) 244 c.members.Insert(user) 245 role = github.RoleMember 246 } 247 return &github.OrgMembership{ 248 Membership: github.Membership{ 249 Role: role, 250 State: state, 251 }, 252 }, nil 253 } 254 255 func (c *fakeClient) ListTeamMembersBySlug(org, teamSlug, role string) ([]github.TeamMember, error) { 256 if teamSlug != configuredTeamSlug { 257 return nil, fmt.Errorf("only team: %s supported, not %s", configuredTeamSlug, teamSlug) 258 } 259 switch role { 260 case github.RoleMember: 261 return c.makeMembers(c.members), nil 262 case github.RoleMaintainer: 263 return c.makeMembers(c.admins), nil 264 default: 265 return nil, fmt.Errorf("fake does not support: %s", role) 266 } 267 } 268 269 func (c *fakeClient) ListTeamInvitationsBySlug(org, teamSlug string) ([]github.OrgInvitation, error) { 270 if teamSlug != configuredTeamSlug { 271 return nil, fmt.Errorf("only team: %s supported, not %s", configuredTeamSlug, teamSlug) 272 } 273 var ret []github.OrgInvitation 274 for p := range c.invitees { 275 if p == "fail" { 276 return nil, errors.New("injected list org invitations failure") 277 } 278 ret = append(ret, github.OrgInvitation{ 279 TeamMember: github.TeamMember{ 280 Login: p, 281 }, 282 }) 283 } 284 return ret, nil 285 } 286 287 const configuredTeamSlug = "team-slug" 288 289 func (c *fakeClient) UpdateTeamMembershipBySlug(org, teamSlug, user string, maintainer bool) (*github.TeamMembership, error) { 290 if teamSlug != configuredTeamSlug { 291 return nil, fmt.Errorf("only team: %s supported, not %s", configuredTeamSlug, teamSlug) 292 } 293 if user == "fail" { 294 return nil, fmt.Errorf("injected failure for %s", user) 295 } 296 var state string 297 if c.orgMembers.Has(user) || len(c.orgMembers) == 0 { 298 state = github.StateActive 299 } else { 300 state = github.StatePending 301 } 302 var role string 303 if maintainer { 304 c.newAdmins.Insert(user) 305 c.admins.Insert(user) 306 role = github.RoleMaintainer 307 } else { 308 c.newMembers.Insert(user) 309 c.members.Insert(user) 310 role = github.RoleMember 311 } 312 return &github.TeamMembership{ 313 Membership: github.Membership{ 314 Role: role, 315 State: state, 316 }, 317 }, nil 318 } 319 320 func (c *fakeClient) RemoveTeamMembershipBySlug(org, teamSlug, user string) error { 321 if teamSlug != configuredTeamSlug { 322 return fmt.Errorf("only team: %s supported, not %s", configuredTeamSlug, teamSlug) 323 } 324 if user == "fail" { 325 return fmt.Errorf("injected failure for %s", user) 326 } 327 c.removed.Insert(user) 328 c.admins.Delete(user) 329 c.members.Delete(user) 330 return nil 331 } 332 333 func TestConfigureMembers(t *testing.T) { 334 cases := []struct { 335 name string 336 want memberships 337 have memberships 338 remove sets.Set[string] 339 members sets.Set[string] 340 supers sets.Set[string] 341 invitees sets.Set[string] 342 err bool 343 }{ 344 { 345 name: "forgot to remove duplicate entry", 346 want: memberships{ 347 members: sets.New[string]("me"), 348 super: sets.New[string]("me"), 349 }, 350 err: true, 351 }, 352 { 353 name: "removal fails", 354 have: memberships{ 355 members: sets.New[string]("fail"), 356 }, 357 err: true, 358 }, 359 { 360 name: "adding admin fails", 361 want: memberships{ 362 super: sets.New[string]("fail"), 363 }, 364 err: true, 365 }, 366 { 367 name: "adding member fails", 368 want: memberships{ 369 members: sets.New[string]("fail"), 370 }, 371 err: true, 372 }, 373 { 374 name: "promote to admin", 375 have: memberships{ 376 members: sets.New[string]("promote"), 377 }, 378 want: memberships{ 379 super: sets.New[string]("promote"), 380 }, 381 supers: sets.New[string]("promote"), 382 }, 383 { 384 name: "downgrade to member", 385 have: memberships{ 386 super: sets.New[string]("downgrade"), 387 }, 388 want: memberships{ 389 members: sets.New[string]("downgrade"), 390 }, 391 members: sets.New[string]("downgrade"), 392 }, 393 { 394 name: "some of everything", 395 have: memberships{ 396 super: sets.New[string]("keep-admin", "drop-admin"), 397 members: sets.New[string]("keep-member", "drop-member"), 398 }, 399 want: memberships{ 400 members: sets.New[string]("keep-member", "new-member"), 401 super: sets.New[string]("keep-admin", "new-admin"), 402 }, 403 remove: sets.New[string]("drop-admin", "drop-member"), 404 members: sets.New[string]("new-member"), 405 supers: sets.New[string]("new-admin"), 406 }, 407 { 408 name: "ensure case insensitivity", 409 have: memberships{ 410 super: sets.New[string]("lower"), 411 members: sets.New[string]("UPPER"), 412 }, 413 want: memberships{ 414 super: sets.New[string]("Lower"), 415 members: sets.New[string]("UpPeR"), 416 }, 417 }, 418 { 419 name: "remove invites for those not in org config", 420 have: memberships{ 421 members: sets.New[string]("member-one", "member-two"), 422 }, 423 want: memberships{ 424 members: sets.New[string]("member-one", "member-two"), 425 }, 426 remove: sets.New[string]("member-three"), 427 invitees: sets.New[string]("member-three"), 428 }, 429 } 430 431 for _, tc := range cases { 432 t.Run(tc.name, func(t *testing.T) { 433 removed := sets.Set[string]{} 434 members := sets.Set[string]{} 435 supers := sets.Set[string]{} 436 adder := func(user string, super bool) error { 437 if user == "fail" { 438 return fmt.Errorf("injected adder failure for %s", user) 439 } 440 if super { 441 supers.Insert(user) 442 } else { 443 members.Insert(user) 444 } 445 return nil 446 } 447 448 remover := func(user string) error { 449 if user == "fail" { 450 return fmt.Errorf("injected remover failure for %s", user) 451 } 452 removed.Insert(user) 453 return nil 454 } 455 456 err := configureMembers(tc.have, tc.want, tc.invitees, adder, remover) 457 switch { 458 case err != nil: 459 if !tc.err { 460 t.Errorf("Unexpected error: %v", err) 461 } 462 case tc.err: 463 t.Errorf("Failed to receive error") 464 default: 465 if err := cmpLists(sets.List(tc.remove), sets.List(removed)); err != nil { 466 t.Errorf("Wrong users removed: %v", err) 467 } else if err := cmpLists(sets.List(tc.members), sets.List(members)); err != nil { 468 t.Errorf("Wrong members added: %v", err) 469 } else if err := cmpLists(sets.List(tc.supers), sets.List(supers)); err != nil { 470 t.Errorf("Wrong supers added: %v", err) 471 } 472 } 473 }) 474 } 475 } 476 477 func TestConfigureOrgMembers(t *testing.T) { 478 cases := []struct { 479 name string 480 opt options 481 config org.Config 482 admins []string 483 members []string 484 invitations []string 485 err bool 486 remove []string 487 addAdmins []string 488 addMembers []string 489 }{ 490 { 491 name: "too few admins", 492 opt: options{ 493 minAdmins: 5, 494 }, 495 config: org.Config{ 496 Admins: []string{"joe"}, 497 }, 498 err: true, 499 }, 500 { 501 name: "remove too many admins", 502 opt: options{ 503 maximumDelta: 0.3, 504 }, 505 config: org.Config{ 506 Admins: []string{"keep", "me"}, 507 }, 508 admins: []string{"a", "b", "c", "keep"}, 509 err: true, 510 }, 511 { 512 name: "forgot to add self", 513 opt: options{ 514 requireSelf: true, 515 }, 516 config: org.Config{ 517 Admins: []string{"other"}, 518 }, 519 err: true, 520 }, 521 { 522 name: "forgot to add required admins", 523 opt: options{ 524 requiredAdmins: flagutil.NewStrings("francis"), 525 }, 526 err: true, 527 }, 528 { 529 name: "can remove self with flag", 530 config: org.Config{}, 531 opt: options{ 532 maximumDelta: 1, 533 requireSelf: false, 534 }, 535 admins: []string{"me"}, 536 remove: []string{"me"}, 537 }, 538 { 539 name: "reject same person with both roles", 540 config: org.Config{ 541 Admins: []string{"me"}, 542 Members: []string{"me"}, 543 }, 544 err: true, 545 }, 546 { 547 name: "github remove rpc fails", 548 admins: []string{"fail"}, 549 err: true, 550 }, 551 { 552 name: "github add rpc fails", 553 config: org.Config{ 554 Admins: []string{"fail"}, 555 }, 556 err: true, 557 }, 558 { 559 name: "require team member to be org member", 560 config: org.Config{ 561 Teams: map[string]org.Team{ 562 "group": { 563 Members: []string{"non-member"}, 564 }, 565 }, 566 }, 567 err: true, 568 }, 569 { 570 name: "require team maintainer to be org member", 571 config: org.Config{ 572 Teams: map[string]org.Team{ 573 "group": { 574 Maintainers: []string{"non-member"}, 575 }, 576 }, 577 }, 578 err: true, 579 }, 580 { 581 name: "require team members with upper name to be org member", 582 config: org.Config{ 583 Teams: map[string]org.Team{ 584 "foo": { 585 Members: []string{"Me"}, 586 }, 587 }, 588 Members: []string{"Me"}, 589 }, 590 members: []string{"Me"}, 591 }, 592 { 593 name: "require team maintainer with upper name to be org member", 594 config: org.Config{ 595 Teams: map[string]org.Team{ 596 "foo": { 597 Maintainers: []string{"Me"}, 598 }, 599 }, 600 Admins: []string{"Me"}, 601 }, 602 admins: []string{"Me"}, 603 }, 604 { 605 name: "disallow duplicate names", 606 config: org.Config{ 607 Teams: map[string]org.Team{ 608 "duplicate": {}, 609 "other": { 610 Previously: []string{"duplicate"}, 611 }, 612 }, 613 }, 614 err: true, 615 }, 616 { 617 name: "disallow duplicate names (single team)", 618 config: org.Config{ 619 Teams: map[string]org.Team{ 620 "foo": { 621 Previously: []string{"foo"}, 622 }, 623 }, 624 }, 625 err: true, 626 }, 627 { 628 name: "trivial case works", 629 }, 630 { 631 name: "some of everything", 632 config: org.Config{ 633 Admins: []string{"keep-admin", "new-admin"}, 634 Members: []string{"keep-member", "new-member"}, 635 }, 636 opt: options{ 637 maximumDelta: 0.5, 638 }, 639 admins: []string{"keep-admin", "drop-admin"}, 640 members: []string{"keep-member", "drop-member"}, 641 remove: []string{"drop-admin", "drop-member"}, 642 addMembers: []string{"new-member"}, 643 addAdmins: []string{"new-admin"}, 644 }, 645 { 646 name: "do not reinvite", 647 config: org.Config{ 648 Admins: []string{"invited-admin"}, 649 Members: []string{"invited-member"}, 650 }, 651 invitations: []string{"invited-admin", "invited-member"}, 652 }, 653 } 654 655 for _, tc := range cases { 656 t.Run(tc.name, func(t *testing.T) { 657 fc := &fakeClient{ 658 admins: sets.New[string](tc.admins...), 659 members: sets.New[string](tc.members...), 660 removed: sets.Set[string]{}, 661 newAdmins: sets.Set[string]{}, 662 newMembers: sets.Set[string]{}, 663 } 664 665 err := configureOrgMembers(tc.opt, fc, fakeOrg, tc.config, sets.New[string](tc.invitations...)) 666 switch { 667 case err != nil: 668 if !tc.err { 669 t.Errorf("Unexpected error: %v", err) 670 } 671 case tc.err: 672 t.Errorf("Failed to receive error") 673 default: 674 if err := cmpLists(tc.remove, sets.List(fc.removed)); err != nil { 675 t.Errorf("Wrong users removed: %v", err) 676 } else if err := cmpLists(tc.addMembers, sets.List(fc.newMembers)); err != nil { 677 t.Errorf("Wrong members added: %v", err) 678 } else if err := cmpLists(tc.addAdmins, sets.List(fc.newAdmins)); err != nil { 679 t.Errorf("Wrong admins added: %v", err) 680 } 681 } 682 }) 683 } 684 } 685 686 type fakeTeamClient struct { 687 teams map[string]github.Team 688 max int 689 } 690 691 func makeFakeTeamClient(teams ...github.Team) *fakeTeamClient { 692 fc := fakeTeamClient{ 693 teams: map[string]github.Team{}, 694 } 695 for _, t := range teams { 696 fc.teams[t.Slug] = t 697 if t.ID >= fc.max { 698 fc.max = t.ID + 1 699 } 700 } 701 return &fc 702 } 703 704 const fakeOrg = "random-org" 705 706 func (c *fakeTeamClient) CreateTeam(org string, team github.Team) (*github.Team, error) { 707 if org != fakeOrg { 708 return nil, fmt.Errorf("org must be %s, not %s", fakeOrg, org) 709 } 710 if team.Name == "fail" { 711 return nil, errors.New("injected CreateTeam error") 712 } 713 c.max++ 714 team.ID = c.max 715 c.teams[team.Slug] = team 716 return &team, nil 717 718 } 719 720 func (c *fakeTeamClient) ListTeams(name string) ([]github.Team, error) { 721 if name == "fail" { 722 return nil, errors.New("injected ListTeams error") 723 } 724 var teams []github.Team 725 for _, t := range c.teams { 726 teams = append(teams, t) 727 } 728 return teams, nil 729 } 730 731 func (c *fakeTeamClient) DeleteTeamBySlug(org, teamSlug string) error { 732 switch _, ok := c.teams[teamSlug]; { 733 case !ok: 734 return fmt.Errorf("not found %s", teamSlug) 735 case teamSlug == "": 736 return errors.New("injected DeleteTeam error") 737 } 738 delete(c.teams, teamSlug) 739 return nil 740 } 741 742 func (c *fakeTeamClient) EditTeam(org string, team github.Team) (*github.Team, error) { 743 slug := team.Slug 744 t, ok := c.teams[slug] 745 if !ok { 746 return nil, fmt.Errorf("team %s does not exist", slug) 747 } 748 switch { 749 case team.Description == "fail": 750 return nil, errors.New("injected description failure") 751 case team.Name == "fail": 752 return nil, errors.New("injected name failure") 753 case team.Privacy == "fail": 754 return nil, errors.New("injected privacy failure") 755 } 756 if team.Description != "" { 757 t.Description = team.Description 758 } 759 if team.Name != "" { 760 t.Name = team.Name 761 } 762 if team.Privacy != "" { 763 t.Privacy = team.Privacy 764 } 765 if team.ParentTeamID != nil { 766 t.Parent = &github.Team{ 767 ID: *team.ParentTeamID, 768 } 769 } else { 770 t.Parent = nil 771 } 772 c.teams[slug] = t 773 return &t, nil 774 } 775 776 func TestFindTeam(t *testing.T) { 777 cases := []struct { 778 name string 779 teams map[string]github.Team 780 current string 781 previous []string 782 expected int 783 }{ 784 { 785 name: "will find current team", 786 teams: map[string]github.Team{ 787 "hello": {ID: 17}, 788 }, 789 current: "hello", 790 expected: 17, 791 }, 792 { 793 name: "team does not exist returns nil", 794 teams: map[string]github.Team{ 795 "unrelated": {ID: 5}, 796 }, 797 current: "hypothetical", 798 }, 799 { 800 name: "will find previous name", 801 teams: map[string]github.Team{ 802 "deprecated name": {ID: 1}, 803 }, 804 current: "current name", 805 previous: []string{"archaic name", "deprecated name"}, 806 expected: 1, 807 }, 808 { 809 name: "prioritize current when previous also exists", 810 teams: map[string]github.Team{ 811 "deprecated": {ID: 1}, 812 "current": {ID: 2}, 813 }, 814 current: "current", 815 previous: []string{"deprecated"}, 816 expected: 2, 817 }, 818 } 819 820 for _, tc := range cases { 821 t.Run(tc.name, func(t *testing.T) { 822 actual := findTeam(tc.teams, tc.current, tc.previous...) 823 switch { 824 case actual == nil: 825 if tc.expected != 0 { 826 t.Errorf("failed to find team %d", tc.expected) 827 } 828 case tc.expected == 0: 829 t.Errorf("unexpected team returned: %v", *actual) 830 case actual.ID != tc.expected: 831 t.Errorf("team %v != expected ID %d", actual, tc.expected) 832 } 833 }) 834 } 835 } 836 837 func TestConfigureTeams(t *testing.T) { 838 desc := "so interesting" 839 priv := org.Secret 840 cases := []struct { 841 name string 842 err bool 843 orgNameOverride string 844 ignoreSecretTeams bool 845 config org.Config 846 teams []github.Team 847 expected map[string]github.Team 848 deleted []string 849 delta float64 850 }{ 851 { 852 name: "do nothing without error", 853 }, 854 { 855 name: "reject duplicated team names (different teams)", 856 err: true, 857 config: org.Config{ 858 Teams: map[string]org.Team{ 859 "hello": {}, 860 "there": {Previously: []string{"hello"}}, 861 }, 862 }, 863 }, 864 { 865 name: "reject duplicated team names (single team)", 866 err: true, 867 config: org.Config{ 868 Teams: map[string]org.Team{ 869 "hello": {Previously: []string{"hello"}}, 870 }, 871 }, 872 }, 873 { 874 name: "fail to list teams", 875 orgNameOverride: "fail", 876 err: true, 877 }, 878 { 879 name: "fail to create team", 880 config: org.Config{ 881 Teams: map[string]org.Team{ 882 "fail": {}, 883 }, 884 }, 885 err: true, 886 }, 887 { 888 name: "fail to delete team", 889 teams: []github.Team{ 890 {Name: "fail", ID: -55}, 891 }, 892 err: true, 893 }, 894 { 895 name: "create missing team", 896 teams: []github.Team{ 897 {Name: "old", ID: 1}, 898 }, 899 config: org.Config{ 900 Teams: map[string]org.Team{ 901 "new": {}, 902 "old": {}, 903 }, 904 }, 905 expected: map[string]github.Team{ 906 "old": {Name: "old", ID: 1}, 907 "new": {Name: "new", ID: 3}, 908 }, 909 }, 910 { 911 name: "reuse existing teams", 912 teams: []github.Team{ 913 {Name: "current", Slug: "current", ID: 1}, 914 {Name: "deprecated", Slug: "deprecated", ID: 5}, 915 }, 916 config: org.Config{ 917 Teams: map[string]org.Team{ 918 "current": {}, 919 "updated": {Previously: []string{"deprecated"}}, 920 }, 921 }, 922 expected: map[string]github.Team{ 923 "current": {Name: "current", Slug: "current", ID: 1}, 924 "updated": {Name: "deprecated", Slug: "deprecated", ID: 5}, 925 }, 926 }, 927 { 928 name: "delete unused teams", 929 teams: []github.Team{ 930 { 931 Name: "unused", 932 Slug: "unused", 933 ID: 1, 934 }, 935 { 936 Name: "used", 937 Slug: "used", 938 ID: 2, 939 }, 940 }, 941 config: org.Config{ 942 Teams: map[string]org.Team{ 943 "used": {}, 944 }, 945 }, 946 expected: map[string]github.Team{ 947 "used": {ID: 2, Name: "used", Slug: "used"}, 948 }, 949 deleted: []string{"unused"}, 950 }, 951 { 952 name: "create team with metadata", 953 config: org.Config{ 954 Teams: map[string]org.Team{ 955 "new": { 956 TeamMetadata: org.TeamMetadata{ 957 Description: &desc, 958 Privacy: &priv, 959 }, 960 }, 961 }, 962 }, 963 expected: map[string]github.Team{ 964 "new": {ID: 1, Name: "new", Description: desc, Privacy: string(priv)}, 965 }, 966 }, 967 { 968 name: "allow deleting many teams", 969 teams: []github.Team{ 970 { 971 Name: "unused", 972 Slug: "unused", 973 ID: 1, 974 }, 975 { 976 Name: "used", 977 Slug: "used", 978 ID: 2, 979 }, 980 }, 981 config: org.Config{ 982 Teams: map[string]org.Team{ 983 "used": {}, 984 }, 985 }, 986 expected: map[string]github.Team{ 987 "used": {ID: 2, Name: "used", Slug: "used"}, 988 }, 989 deleted: []string{"unused"}, 990 delta: 0.6, 991 }, 992 { 993 name: "refuse to delete too many teams", 994 teams: []github.Team{ 995 { 996 Name: "unused", 997 Slug: "unused", 998 ID: 1, 999 }, 1000 { 1001 Name: "used", 1002 Slug: "used", 1003 ID: 2, 1004 }, 1005 }, 1006 config: org.Config{ 1007 Teams: map[string]org.Team{ 1008 "used": {}, 1009 }, 1010 }, 1011 err: true, 1012 delta: 0.1, 1013 }, 1014 { 1015 name: "refuse to delete private teams if ignoring them", 1016 ignoreSecretTeams: true, 1017 teams: []github.Team{ 1018 { 1019 Name: "secret", 1020 Slug: "secret", 1021 ID: 1, 1022 Privacy: string(org.Secret), 1023 }, 1024 { 1025 Name: "closed", 1026 Slug: "closed", 1027 ID: 2, 1028 Privacy: string(org.Closed), 1029 }, 1030 }, 1031 config: org.Config{Teams: map[string]org.Team{}}, 1032 err: false, 1033 expected: map[string]github.Team{}, 1034 deleted: []string{"closed"}, 1035 delta: 1, 1036 }, 1037 } 1038 1039 for _, tc := range cases { 1040 t.Run(tc.name, func(t *testing.T) { 1041 fc := makeFakeTeamClient(tc.teams...) 1042 orgName := tc.orgNameOverride 1043 if orgName == "" { 1044 orgName = fakeOrg 1045 } 1046 if tc.expected == nil { 1047 tc.expected = map[string]github.Team{} 1048 } 1049 if tc.delta == 0 { 1050 tc.delta = 1 1051 } 1052 actual, err := configureTeams(fc, orgName, tc.config, tc.delta, tc.ignoreSecretTeams) 1053 switch { 1054 case err != nil: 1055 if !tc.err { 1056 t.Errorf("unexpected error: %v", err) 1057 } 1058 case tc.err: 1059 t.Errorf("failed to receive error") 1060 case !reflect.DeepEqual(actual, tc.expected): 1061 t.Errorf("%#v != actual %#v", tc.expected, actual) 1062 } 1063 for _, slug := range tc.deleted { 1064 if team, ok := fc.teams[slug]; ok { 1065 t.Errorf("%s still present: %#v", slug, team) 1066 } 1067 } 1068 original, current, deleted := sets.New[string](), sets.New[string](), sets.New[string](tc.deleted...) 1069 for _, team := range tc.teams { 1070 original.Insert(team.Slug) 1071 } 1072 for slug := range fc.teams { 1073 current.Insert(slug) 1074 } 1075 if unexpected := original.Difference(current).Difference(deleted); unexpected.Len() > 0 { 1076 t.Errorf("the following teams were unexpectedly deleted: %v", sets.List(unexpected)) 1077 } 1078 }) 1079 } 1080 } 1081 1082 func TestConfigureTeam(t *testing.T) { 1083 old := "old value" 1084 cur := "current value" 1085 fail := "fail" 1086 pfail := org.Privacy(fail) 1087 whatev := "whatever" 1088 secret := org.Secret 1089 parent := 2 1090 cases := []struct { 1091 name string 1092 err bool 1093 teamName string 1094 parent *int 1095 config org.Team 1096 github github.Team 1097 expected github.Team 1098 }{ 1099 { 1100 name: "patch team when name changes", 1101 teamName: cur, 1102 config: org.Team{ 1103 Previously: []string{old}, 1104 }, 1105 github: github.Team{ 1106 ID: 1, 1107 Name: old, 1108 }, 1109 expected: github.Team{ 1110 ID: 1, 1111 Name: cur, 1112 }, 1113 }, 1114 { 1115 name: "patch team when description changes", 1116 teamName: whatev, 1117 parent: nil, 1118 config: org.Team{ 1119 TeamMetadata: org.TeamMetadata{ 1120 Description: &cur, 1121 }, 1122 }, 1123 github: github.Team{ 1124 ID: 2, 1125 Name: whatev, 1126 Description: old, 1127 }, 1128 expected: github.Team{ 1129 ID: 2, 1130 Name: whatev, 1131 Description: cur, 1132 }, 1133 }, 1134 { 1135 name: "patch team when privacy changes", 1136 teamName: whatev, 1137 parent: nil, 1138 config: org.Team{ 1139 TeamMetadata: org.TeamMetadata{ 1140 Privacy: &secret, 1141 }, 1142 }, 1143 github: github.Team{ 1144 ID: 3, 1145 Name: whatev, 1146 Privacy: string(org.Closed), 1147 }, 1148 expected: github.Team{ 1149 ID: 3, 1150 Name: whatev, 1151 Privacy: string(secret), 1152 }, 1153 }, 1154 { 1155 name: "patch team when parent changes", 1156 teamName: whatev, 1157 parent: &parent, 1158 config: org.Team{}, 1159 github: github.Team{ 1160 ID: 3, 1161 Name: whatev, 1162 Parent: &github.Team{ 1163 ID: 4, 1164 }, 1165 }, 1166 expected: github.Team{ 1167 ID: 3, 1168 Name: whatev, 1169 Parent: &github.Team{ 1170 ID: 2, 1171 }, 1172 Privacy: string(org.Closed), 1173 }, 1174 }, 1175 { 1176 name: "patch team when parent removed", 1177 teamName: whatev, 1178 parent: nil, 1179 config: org.Team{}, 1180 github: github.Team{ 1181 ID: 3, 1182 Name: whatev, 1183 Parent: &github.Team{ 1184 ID: 2, 1185 }, 1186 }, 1187 expected: github.Team{ 1188 ID: 3, 1189 Name: whatev, 1190 Parent: nil, 1191 }, 1192 }, 1193 { 1194 name: "do not patch team when values are the same", 1195 teamName: fail, 1196 parent: &parent, 1197 config: org.Team{ 1198 TeamMetadata: org.TeamMetadata{ 1199 Description: &fail, 1200 Privacy: &pfail, 1201 }, 1202 }, 1203 github: github.Team{ 1204 ID: 4, 1205 Name: fail, 1206 Description: fail, 1207 Privacy: fail, 1208 Parent: &github.Team{ 1209 ID: 2, 1210 }, 1211 }, 1212 expected: github.Team{ 1213 ID: 4, 1214 Name: fail, 1215 Description: fail, 1216 Privacy: fail, 1217 Parent: &github.Team{ 1218 ID: 2, 1219 }, 1220 }, 1221 }, 1222 { 1223 name: "fail to patch team", 1224 teamName: "team", 1225 parent: nil, 1226 config: org.Team{ 1227 TeamMetadata: org.TeamMetadata{ 1228 Description: &fail, 1229 }, 1230 }, 1231 github: github.Team{ 1232 ID: 1, 1233 Name: "team", 1234 Description: whatev, 1235 }, 1236 err: true, 1237 }, 1238 } 1239 1240 for _, tc := range cases { 1241 t.Run(tc.name, func(t *testing.T) { 1242 fc := makeFakeTeamClient(tc.github) 1243 err := configureTeam(fc, fakeOrg, tc.teamName, tc.config, tc.github, tc.parent) 1244 switch { 1245 case err != nil: 1246 if !tc.err { 1247 t.Errorf("unexpected error: %v", err) 1248 } 1249 case tc.err: 1250 t.Errorf("failed to receive expected error") 1251 case !reflect.DeepEqual(fc.teams[tc.expected.Slug], tc.expected): 1252 t.Errorf("actual %+v != expected %+v", fc.teams[tc.expected.Slug], tc.expected) 1253 } 1254 }) 1255 } 1256 } 1257 1258 func TestConfigureTeamMembers(t *testing.T) { 1259 cases := []struct { 1260 name string 1261 err bool 1262 members sets.Set[string] 1263 maintainers sets.Set[string] 1264 remove sets.Set[string] 1265 addMembers sets.Set[string] 1266 addMaintainers sets.Set[string] 1267 ignoreInvitees bool 1268 invitees sets.Set[string] 1269 team org.Team 1270 slug string 1271 }{ 1272 { 1273 name: "fail when listing fails", 1274 slug: "some-slug", 1275 err: true, 1276 }, 1277 { 1278 name: "fail when removal fails", 1279 members: sets.New[string]("fail"), 1280 err: true, 1281 }, 1282 { 1283 name: "fail when add fails", 1284 team: org.Team{ 1285 Maintainers: []string{"fail"}, 1286 }, 1287 err: true, 1288 }, 1289 { 1290 name: "some of everything", 1291 team: org.Team{ 1292 Maintainers: []string{"keep-maintainer", "new-maintainer"}, 1293 Members: []string{"keep-member", "new-member"}, 1294 }, 1295 maintainers: sets.New[string]("keep-maintainer", "drop-maintainer"), 1296 members: sets.New[string]("keep-member", "drop-member"), 1297 remove: sets.New[string]("drop-maintainer", "drop-member"), 1298 addMembers: sets.New[string]("new-member"), 1299 addMaintainers: sets.New[string]("new-maintainer"), 1300 }, 1301 { 1302 name: "do not reinvitee invitees", 1303 team: org.Team{ 1304 Maintainers: []string{"invited-maintainer", "newbie"}, 1305 Members: []string{"invited-member"}, 1306 }, 1307 invitees: sets.New[string]("invited-maintainer", "invited-member"), 1308 addMaintainers: sets.New[string]("newbie"), 1309 }, 1310 { 1311 name: "do not remove pending invitees", 1312 team: org.Team{ 1313 Maintainers: []string{"keep-maintainer"}, 1314 Members: []string{"invited-member"}, 1315 }, 1316 maintainers: sets.New[string]("keep-maintainer"), 1317 invitees: sets.New[string]("invited-member"), 1318 remove: sets.Set[string]{}, 1319 }, 1320 { 1321 name: "ignore invitees", 1322 team: org.Team{ 1323 Maintainers: []string{"keep-maintainer"}, 1324 Members: []string{"keep-member", "new-member"}, 1325 }, 1326 maintainers: sets.New[string]("keep-maintainer"), 1327 members: sets.New[string]("keep-member"), 1328 invitees: sets.Set[string]{}, 1329 remove: sets.Set[string]{}, 1330 addMembers: sets.New[string]("new-member"), 1331 ignoreInvitees: true, 1332 }, 1333 } 1334 1335 for _, tc := range cases { 1336 gt := github.Team{ 1337 Slug: configuredTeamSlug, 1338 Name: "whatev", 1339 } 1340 if tc.slug != "" { 1341 gt.Slug = tc.slug 1342 } 1343 t.Run(tc.name, func(t *testing.T) { 1344 fc := &fakeClient{ 1345 admins: sets.KeySet[string](tc.maintainers), 1346 members: sets.KeySet[string](tc.members), 1347 invitees: sets.KeySet[string](tc.invitees), 1348 removed: sets.Set[string]{}, 1349 newAdmins: sets.Set[string]{}, 1350 newMembers: sets.Set[string]{}, 1351 } 1352 err := configureTeamMembers(fc, "", gt, tc.team, tc.ignoreInvitees) 1353 switch { 1354 case err != nil: 1355 if !tc.err { 1356 t.Errorf("Unexpected error: %v", err) 1357 } 1358 case tc.err: 1359 t.Errorf("Failed to receive error") 1360 default: 1361 if err := cmpLists(sets.List(tc.remove), sets.List(fc.removed)); err != nil { 1362 t.Errorf("Wrong users removed: %v", err) 1363 } else if err := cmpLists(sets.List(tc.addMembers), sets.List(fc.newMembers)); err != nil { 1364 t.Errorf("Wrong members added: %v", err) 1365 } else if err := cmpLists(sets.List(tc.addMaintainers), sets.List(fc.newAdmins)); err != nil { 1366 t.Errorf("Wrong admins added: %v", err) 1367 } 1368 } 1369 1370 }) 1371 } 1372 } 1373 1374 func cmpLists(a, b []string) error { 1375 if a == nil { 1376 a = []string{} 1377 } 1378 if b == nil { 1379 b = []string{} 1380 } 1381 sort.Strings(a) 1382 sort.Strings(b) 1383 if !reflect.DeepEqual(a, b) { 1384 return fmt.Errorf("%v != %v", a, b) 1385 } 1386 return nil 1387 } 1388 1389 type fakeOrgClient struct { 1390 current github.Organization 1391 changed bool 1392 } 1393 1394 func (o *fakeOrgClient) GetOrg(name string) (*github.Organization, error) { 1395 if name == "fail" { 1396 return nil, errors.New("injected GetOrg error") 1397 } 1398 return &o.current, nil 1399 } 1400 1401 func (o *fakeOrgClient) EditOrg(name string, org github.Organization) (*github.Organization, error) { 1402 if org.Description == "fail" { 1403 return nil, errors.New("injected EditOrg error") 1404 } 1405 o.current = org 1406 o.changed = true 1407 return &o.current, nil 1408 } 1409 1410 func TestUpdateBool(t *testing.T) { 1411 yes := true 1412 no := false 1413 cases := []struct { 1414 name string 1415 have *bool 1416 want *bool 1417 end bool 1418 ret *bool 1419 }{ 1420 { 1421 name: "panic on nil have", 1422 want: &no, 1423 }, 1424 { 1425 name: "never change on nil want", 1426 want: nil, 1427 have: &yes, 1428 end: yes, 1429 ret: &no, 1430 }, 1431 { 1432 name: "do not change if same", 1433 want: &yes, 1434 have: &yes, 1435 end: yes, 1436 ret: &no, 1437 }, 1438 { 1439 name: "change if different", 1440 want: &no, 1441 have: &yes, 1442 end: no, 1443 ret: &yes, 1444 }, 1445 } 1446 1447 for _, tc := range cases { 1448 t.Run(tc.name, func(t *testing.T) { 1449 defer func() { 1450 wantPanic := tc.ret == nil 1451 r := recover() 1452 gotPanic := r != nil 1453 switch { 1454 case gotPanic && !wantPanic: 1455 t.Errorf("unexpected panic: %v", r) 1456 case wantPanic && !gotPanic: 1457 t.Errorf("failed to receive panic") 1458 } 1459 }() 1460 if tc.have != nil { // prevent overwriting what tc.have points to for next test case 1461 have := *tc.have 1462 tc.have = &have 1463 } 1464 ret := updateBool(tc.have, tc.want) 1465 switch { 1466 case ret != *tc.ret: 1467 t.Errorf("return value %t != expected %t", ret, *tc.ret) 1468 case *tc.have != tc.end: 1469 t.Errorf("end value %t != expected %t", *tc.have, tc.end) 1470 } 1471 }) 1472 } 1473 } 1474 1475 func TestUpdateString(t *testing.T) { 1476 no := false 1477 yes := true 1478 hello := "hello" 1479 world := "world" 1480 empty := "" 1481 cases := []struct { 1482 name string 1483 have *string 1484 want *string 1485 expected string 1486 ret *bool 1487 }{ 1488 { 1489 name: "panic on nil have", 1490 want: &hello, 1491 }, 1492 { 1493 name: "never change on nil want", 1494 want: nil, 1495 have: &hello, 1496 expected: hello, 1497 ret: &no, 1498 }, 1499 { 1500 name: "do not change if same", 1501 want: &world, 1502 have: &world, 1503 expected: world, 1504 ret: &no, 1505 }, 1506 { 1507 name: "change if different", 1508 want: &empty, 1509 have: &hello, 1510 expected: empty, 1511 ret: &yes, 1512 }, 1513 } 1514 1515 for _, tc := range cases { 1516 t.Run(tc.name, func(t *testing.T) { 1517 defer func() { 1518 wantPanic := tc.ret == nil 1519 r := recover() 1520 gotPanic := r != nil 1521 switch { 1522 case gotPanic && !wantPanic: 1523 t.Errorf("unexpected panic: %v", r) 1524 case wantPanic && !gotPanic: 1525 t.Errorf("failed to receive panic") 1526 } 1527 }() 1528 if tc.have != nil { // prevent overwriting what tc.have points to for next test case 1529 have := *tc.have 1530 tc.have = &have 1531 } 1532 ret := updateString(tc.have, tc.want) 1533 switch { 1534 case ret != *tc.ret: 1535 t.Errorf("return value %t != expected %t", ret, *tc.ret) 1536 case *tc.have != tc.expected: 1537 t.Errorf("end value %s != expected %s", *tc.have, tc.expected) 1538 } 1539 }) 1540 } 1541 } 1542 1543 func TestConfigureOrgMeta(t *testing.T) { 1544 filled := github.Organization{ 1545 BillingEmail: "be", 1546 Company: "co", 1547 Email: "em", 1548 Location: "lo", 1549 Name: "na", 1550 Description: "de", 1551 HasOrganizationProjects: true, 1552 HasRepositoryProjects: true, 1553 DefaultRepositoryPermission: "not-a-real-value", 1554 MembersCanCreateRepositories: true, 1555 } 1556 yes := true 1557 no := false 1558 str := "random-letters" 1559 fail := "fail" 1560 read := github.Read 1561 1562 cases := []struct { 1563 name string 1564 orgName string 1565 want org.Metadata 1566 have github.Organization 1567 expected github.Organization 1568 err bool 1569 change bool 1570 }{ 1571 { 1572 name: "no want means no change", 1573 have: filled, 1574 expected: filled, 1575 change: false, 1576 }, 1577 { 1578 name: "fail if GetOrg fails", 1579 orgName: fail, 1580 err: true, 1581 }, 1582 { 1583 name: "fail if EditOrg fails", 1584 want: org.Metadata{Description: &fail}, 1585 err: true, 1586 }, 1587 { 1588 name: "billing diff causes change", 1589 want: org.Metadata{BillingEmail: &str}, 1590 expected: github.Organization{ 1591 BillingEmail: str, 1592 }, 1593 change: true, 1594 }, 1595 { 1596 name: "company diff causes change", 1597 want: org.Metadata{Company: &str}, 1598 expected: github.Organization{ 1599 Company: str, 1600 }, 1601 change: true, 1602 }, 1603 { 1604 name: "email diff causes change", 1605 want: org.Metadata{Email: &str}, 1606 expected: github.Organization{ 1607 Email: str, 1608 }, 1609 change: true, 1610 }, 1611 { 1612 name: "location diff causes change", 1613 want: org.Metadata{Location: &str}, 1614 expected: github.Organization{ 1615 Location: str, 1616 }, 1617 change: true, 1618 }, 1619 { 1620 name: "name diff causes change", 1621 want: org.Metadata{Name: &str}, 1622 expected: github.Organization{ 1623 Name: str, 1624 }, 1625 change: true, 1626 }, 1627 { 1628 name: "org projects diff causes change", 1629 want: org.Metadata{HasOrganizationProjects: &yes}, 1630 expected: github.Organization{ 1631 HasOrganizationProjects: yes, 1632 }, 1633 change: true, 1634 }, 1635 { 1636 name: "repo projects diff causes change", 1637 want: org.Metadata{HasRepositoryProjects: &yes}, 1638 expected: github.Organization{ 1639 HasRepositoryProjects: yes, 1640 }, 1641 change: true, 1642 }, 1643 { 1644 name: "default permission diff causes change", 1645 want: org.Metadata{DefaultRepositoryPermission: &read}, 1646 expected: github.Organization{ 1647 DefaultRepositoryPermission: string(read), 1648 }, 1649 change: true, 1650 }, 1651 { 1652 name: "members can create diff causes change", 1653 want: org.Metadata{MembersCanCreateRepositories: &yes}, 1654 expected: github.Organization{ 1655 MembersCanCreateRepositories: yes, 1656 }, 1657 change: true, 1658 }, 1659 { 1660 name: "change all values at once", 1661 have: filled, 1662 want: org.Metadata{ 1663 BillingEmail: &str, 1664 Company: &str, 1665 Email: &str, 1666 Location: &str, 1667 Name: &str, 1668 Description: &str, 1669 HasOrganizationProjects: &no, 1670 HasRepositoryProjects: &no, 1671 MembersCanCreateRepositories: &no, 1672 DefaultRepositoryPermission: &read, 1673 }, 1674 expected: github.Organization{ 1675 BillingEmail: str, 1676 Company: str, 1677 Email: str, 1678 Location: str, 1679 Name: str, 1680 Description: str, 1681 HasOrganizationProjects: no, 1682 HasRepositoryProjects: no, 1683 MembersCanCreateRepositories: no, 1684 DefaultRepositoryPermission: string(read), 1685 }, 1686 change: true, 1687 }, 1688 } 1689 1690 for _, tc := range cases { 1691 t.Run(tc.name, func(t *testing.T) { 1692 if tc.orgName == "" { 1693 tc.orgName = "whatever" 1694 } 1695 fc := fakeOrgClient{ 1696 current: tc.have, 1697 } 1698 err := configureOrgMeta(&fc, tc.orgName, tc.want) 1699 switch { 1700 case err != nil: 1701 if !tc.err { 1702 t.Errorf("unexpected error: %v", err) 1703 } 1704 case tc.err: 1705 t.Errorf("failed to receive error") 1706 case tc.change != fc.changed: 1707 t.Errorf("changed %t != expected %t", fc.changed, tc.change) 1708 case !reflect.DeepEqual(fc.current, tc.expected): 1709 t.Errorf("current %#v != expected %#v", fc.current, tc.expected) 1710 } 1711 }) 1712 } 1713 } 1714 1715 func TestDumpOrgConfig(t *testing.T) { 1716 empty := "" 1717 hello := "Hello" 1718 details := "wise and brilliant exemplary human specimens" 1719 yes := true 1720 no := false 1721 perm := github.Write 1722 pub := org.Privacy("") 1723 secret := org.Secret 1724 closed := org.Closed 1725 repoName := "project" 1726 repoDescription := "awesome testing project" 1727 repoHomepage := "https://www.somewhe.re/something/" 1728 master := "master-branch" 1729 cases := []struct { 1730 name string 1731 orgOverride string 1732 ignoreSecretTeams bool 1733 meta github.Organization 1734 members []string 1735 admins []string 1736 teams []github.Team 1737 teamMembers map[string][]string 1738 maintainers map[string][]string 1739 repoPermissions map[string][]github.Repo 1740 repos []github.FullRepo 1741 expected org.Config 1742 err bool 1743 }{ 1744 { 1745 name: "fails if GetOrg fails", 1746 orgOverride: "fail", 1747 err: true, 1748 }, 1749 { 1750 name: "fails if ListOrgMembers fails", 1751 err: true, 1752 members: []string{"hello", "fail"}, 1753 }, 1754 { 1755 name: "fails if ListTeams fails", 1756 err: true, 1757 teams: []github.Team{ 1758 { 1759 Name: "fail", 1760 ID: 3, 1761 }, 1762 }, 1763 }, 1764 { 1765 name: "fails if ListTeamMembersFails", 1766 err: true, 1767 teams: []github.Team{ 1768 { 1769 Name: "fred", 1770 ID: -1, 1771 }, 1772 }, 1773 }, 1774 { 1775 name: "fails if GetTeams fails", 1776 err: true, 1777 repos: []github.FullRepo{ 1778 { 1779 Repo: github.Repo{ 1780 Name: "fail", 1781 }, 1782 }, 1783 }, 1784 }, 1785 { 1786 name: "fails if not as an admin of the org", 1787 err: true, 1788 admins: []string{"not-admin"}, 1789 }, 1790 { 1791 name: "basically works", 1792 meta: github.Organization{ 1793 Name: hello, 1794 MembersCanCreateRepositories: yes, 1795 DefaultRepositoryPermission: string(perm), 1796 }, 1797 members: []string{"george", "jungle", "banana"}, 1798 admins: []string{"admin", "james", "giant", "peach"}, 1799 teams: []github.Team{ 1800 { 1801 ID: 5, 1802 Slug: "team-5", 1803 Name: "friends", 1804 Description: details, 1805 }, 1806 { 1807 ID: 6, 1808 Slug: "team-6", 1809 Name: "enemies", 1810 }, 1811 { 1812 ID: 7, 1813 Slug: "team-7", 1814 Name: "archenemies", 1815 Parent: &github.Team{ 1816 ID: 6, 1817 Slug: "team-6", 1818 Name: "enemies", 1819 }, 1820 Privacy: string(org.Secret), 1821 }, 1822 }, 1823 teamMembers: map[string][]string{ 1824 "team-5": {"george", "james"}, 1825 "team-6": {"george"}, 1826 "team-7": {}, 1827 }, 1828 maintainers: map[string][]string{ 1829 "team-5": {}, 1830 "team-6": {"giant", "jungle"}, 1831 "team-7": {"banana"}, 1832 }, 1833 repoPermissions: map[string][]github.Repo{ 1834 "team-5": {}, 1835 "team-6": {{Name: "pull-repo", Permissions: github.RepoPermissions{Pull: true}}}, 1836 "team-7": {{Name: "pull-repo", Permissions: github.RepoPermissions{Pull: true}}, {Name: "admin-repo", Permissions: github.RepoPermissions{Admin: true}}}, 1837 }, 1838 repos: []github.FullRepo{ 1839 { 1840 Repo: github.Repo{ 1841 Name: repoName, 1842 Description: repoDescription, 1843 Homepage: repoHomepage, 1844 Private: false, 1845 HasIssues: true, 1846 HasProjects: true, 1847 HasWiki: true, 1848 Archived: true, 1849 DefaultBranch: master, 1850 }, 1851 }, 1852 }, 1853 expected: org.Config{ 1854 Metadata: org.Metadata{ 1855 Name: &hello, 1856 BillingEmail: &empty, 1857 Company: &empty, 1858 Email: &empty, 1859 Description: &empty, 1860 Location: &empty, 1861 HasOrganizationProjects: &no, 1862 HasRepositoryProjects: &no, 1863 DefaultRepositoryPermission: &perm, 1864 MembersCanCreateRepositories: &yes, 1865 }, 1866 Teams: map[string]org.Team{ 1867 "friends": { 1868 TeamMetadata: org.TeamMetadata{ 1869 Description: &details, 1870 Privacy: &pub, 1871 }, 1872 Members: []string{"george", "james"}, 1873 Maintainers: []string{}, 1874 Children: map[string]org.Team{}, 1875 Repos: map[string]github.RepoPermissionLevel{}, 1876 }, 1877 "enemies": { 1878 TeamMetadata: org.TeamMetadata{ 1879 Description: &empty, 1880 Privacy: &pub, 1881 }, 1882 Members: []string{"george"}, 1883 Maintainers: []string{"giant", "jungle"}, 1884 Repos: map[string]github.RepoPermissionLevel{ 1885 "pull-repo": github.Read, 1886 }, 1887 Children: map[string]org.Team{ 1888 "archenemies": { 1889 TeamMetadata: org.TeamMetadata{ 1890 Description: &empty, 1891 Privacy: &secret, 1892 }, 1893 Members: []string{}, 1894 Maintainers: []string{"banana"}, 1895 Repos: map[string]github.RepoPermissionLevel{ 1896 "pull-repo": github.Read, 1897 "admin-repo": github.Admin, 1898 }, 1899 Children: map[string]org.Team{}, 1900 }, 1901 }, 1902 }, 1903 }, 1904 Members: []string{"george", "jungle", "banana"}, 1905 Admins: []string{"admin", "james", "giant", "peach"}, 1906 Repos: map[string]org.Repo{ 1907 "project": { 1908 Description: &repoDescription, 1909 HomePage: &repoHomepage, 1910 HasProjects: &yes, 1911 AllowMergeCommit: &no, 1912 AllowRebaseMerge: &no, 1913 AllowSquashMerge: &no, 1914 Archived: &yes, 1915 DefaultBranch: &master, 1916 }, 1917 }, 1918 }, 1919 }, 1920 { 1921 name: "ignores private teams when expected to", 1922 ignoreSecretTeams: true, 1923 meta: github.Organization{ 1924 Name: hello, 1925 MembersCanCreateRepositories: yes, 1926 DefaultRepositoryPermission: string(perm), 1927 }, 1928 members: []string{"george", "jungle", "banana"}, 1929 admins: []string{"admin", "james", "giant", "peach"}, 1930 teams: []github.Team{ 1931 { 1932 ID: 5, 1933 Slug: "team-5", 1934 Name: "friends", 1935 Description: details, 1936 }, 1937 { 1938 ID: 6, 1939 Slug: "team-6", 1940 Name: "enemies", 1941 }, 1942 { 1943 ID: 7, 1944 Slug: "team-7", 1945 Name: "archenemies", 1946 Parent: &github.Team{ 1947 Slug: "team-6", 1948 Name: "enemies", 1949 }, 1950 Privacy: string(org.Secret), 1951 }, 1952 { 1953 ID: 8, 1954 Slug: "team-8", 1955 Name: "frenemies", 1956 Parent: &github.Team{ 1957 ID: 6, 1958 Slug: "team-6", 1959 Name: "enemies", 1960 }, 1961 Privacy: string(org.Closed), 1962 }, 1963 }, 1964 teamMembers: map[string][]string{ 1965 "team-5": {"george", "james"}, 1966 "team-6": {"george"}, 1967 "team-7": {}, 1968 "team-8": {"patrick"}, 1969 }, 1970 maintainers: map[string][]string{ 1971 "team-5": {}, 1972 "team-6": {"giant", "jungle"}, 1973 "team-7": {"banana"}, 1974 "team-8": {"starfish"}, 1975 }, 1976 expected: org.Config{ 1977 Metadata: org.Metadata{ 1978 Name: &hello, 1979 BillingEmail: &empty, 1980 Company: &empty, 1981 Email: &empty, 1982 Description: &empty, 1983 Location: &empty, 1984 HasOrganizationProjects: &no, 1985 HasRepositoryProjects: &no, 1986 DefaultRepositoryPermission: &perm, 1987 MembersCanCreateRepositories: &yes, 1988 }, 1989 Teams: map[string]org.Team{ 1990 "friends": { 1991 TeamMetadata: org.TeamMetadata{ 1992 Description: &details, 1993 Privacy: &pub, 1994 }, 1995 Members: []string{"george", "james"}, 1996 Maintainers: []string{}, 1997 Children: map[string]org.Team{}, 1998 Repos: map[string]github.RepoPermissionLevel{}, 1999 }, 2000 "enemies": { 2001 TeamMetadata: org.TeamMetadata{ 2002 Description: &empty, 2003 Privacy: &pub, 2004 }, 2005 Members: []string{"george"}, 2006 Maintainers: []string{"giant", "jungle"}, 2007 Children: map[string]org.Team{ 2008 "frenemies": { 2009 TeamMetadata: org.TeamMetadata{ 2010 Description: &empty, 2011 Privacy: &closed, 2012 }, 2013 Members: []string{"patrick"}, 2014 Maintainers: []string{"starfish"}, 2015 Children: map[string]org.Team{}, 2016 Repos: map[string]github.RepoPermissionLevel{}, 2017 }, 2018 }, 2019 Repos: map[string]github.RepoPermissionLevel{}, 2020 }, 2021 }, 2022 Members: []string{"george", "jungle", "banana"}, 2023 Admins: []string{"admin", "james", "giant", "peach"}, 2024 Repos: map[string]org.Repo{}, 2025 }, 2026 }, 2027 } 2028 2029 for _, tc := range cases { 2030 t.Run(tc.name, func(t *testing.T) { 2031 orgName := "random-org" 2032 if tc.orgOverride != "" { 2033 orgName = tc.orgOverride 2034 } 2035 fc := fakeDumpClient{ 2036 name: orgName, 2037 members: tc.members, 2038 admins: tc.admins, 2039 meta: tc.meta, 2040 teams: tc.teams, 2041 teamMembers: tc.teamMembers, 2042 maintainers: tc.maintainers, 2043 repoPermissions: tc.repoPermissions, 2044 repos: tc.repos, 2045 } 2046 actual, err := dumpOrgConfig(fc, orgName, tc.ignoreSecretTeams, "") 2047 switch { 2048 case err != nil: 2049 if !tc.err { 2050 t.Errorf("unexpected error: %v", err) 2051 } 2052 case tc.err: 2053 t.Errorf("failed to receive error") 2054 default: 2055 fixup(actual) 2056 fixup(&tc.expected) 2057 if diff := cmp.Diff(actual, &tc.expected); diff != "" { 2058 t.Errorf("did not get correct config, diff: %s", diff) 2059 } 2060 2061 } 2062 }) 2063 } 2064 } 2065 2066 type fakeDumpClient struct { 2067 name string 2068 members []string 2069 admins []string 2070 meta github.Organization 2071 teams []github.Team 2072 teamMembers map[string][]string 2073 maintainers map[string][]string 2074 repoPermissions map[string][]github.Repo 2075 repos []github.FullRepo 2076 } 2077 2078 func (c fakeDumpClient) GetOrg(name string) (*github.Organization, error) { 2079 if name != c.name { 2080 return nil, errors.New("bad name") 2081 } 2082 if name == "fail" { 2083 return nil, errors.New("injected GetOrg error") 2084 } 2085 return &c.meta, nil 2086 } 2087 2088 func (c fakeDumpClient) makeMembers(people []string) ([]github.TeamMember, error) { 2089 var ret []github.TeamMember 2090 for _, p := range people { 2091 if p == "fail" { 2092 return nil, errors.New("injected makeMembers error") 2093 } 2094 ret = append(ret, github.TeamMember{Login: p}) 2095 } 2096 return ret, nil 2097 } 2098 2099 func (c fakeDumpClient) ListOrgMembers(name, role string) ([]github.TeamMember, error) { 2100 switch { 2101 case name != c.name: 2102 return nil, fmt.Errorf("bad org: %s", name) 2103 case role == github.RoleAdmin: 2104 return c.makeMembers(c.admins) 2105 case role == github.RoleMember: 2106 return c.makeMembers(c.members) 2107 } 2108 return nil, fmt.Errorf("bad role: %s", role) 2109 } 2110 2111 func (c fakeDumpClient) ListTeams(name string) ([]github.Team, error) { 2112 if name != c.name { 2113 return nil, fmt.Errorf("bad org: %s", name) 2114 } 2115 2116 for _, t := range c.teams { 2117 if t.Name == "fail" { 2118 return nil, errors.New("injected ListTeams error") 2119 } 2120 } 2121 return c.teams, nil 2122 } 2123 2124 func (c fakeDumpClient) ListTeamMembersBySlug(org, teamSlug, role string) ([]github.TeamMember, error) { 2125 var mapping map[string][]string 2126 switch { 2127 case teamSlug == "": 2128 return nil, errors.New("injected ListTeamMembers error") 2129 case role == github.RoleMaintainer: 2130 mapping = c.maintainers 2131 case role == github.RoleMember: 2132 mapping = c.teamMembers 2133 default: 2134 return nil, fmt.Errorf("bad role: %s", role) 2135 } 2136 people, ok := mapping[teamSlug] 2137 if !ok { 2138 return nil, fmt.Errorf("team does not exist: %s", teamSlug) 2139 } 2140 return c.makeMembers(people) 2141 } 2142 2143 func (c fakeDumpClient) ListTeamReposBySlug(org, teamSlug string) ([]github.Repo, error) { 2144 if teamSlug == "" { 2145 return nil, errors.New("injected ListTeamRepos error") 2146 } 2147 2148 return c.repoPermissions[teamSlug], nil 2149 } 2150 2151 func (c fakeDumpClient) GetRepos(org string, isUser bool) ([]github.Repo, error) { 2152 var repos []github.Repo 2153 for _, repo := range c.repos { 2154 if repo.Name == "fail" { 2155 return nil, fmt.Errorf("injected GetRepos error") 2156 } 2157 repos = append(repos, repo.Repo) 2158 } 2159 2160 return repos, nil 2161 } 2162 2163 func (c fakeDumpClient) GetRepo(owner, repo string) (github.FullRepo, error) { 2164 for _, r := range c.repos { 2165 switch { 2166 case r.Name == "fail": 2167 return r, fmt.Errorf("injected GetRepo error") 2168 case r.Name == repo: 2169 return r, nil 2170 } 2171 } 2172 2173 return github.FullRepo{}, fmt.Errorf("not found") 2174 } 2175 2176 func (c fakeDumpClient) BotUser() (*github.UserData, error) { 2177 return &github.UserData{Login: "admin"}, nil 2178 } 2179 2180 func fixup(ret *org.Config) { 2181 if ret == nil { 2182 return 2183 } 2184 sort.Strings(ret.Members) 2185 sort.Strings(ret.Admins) 2186 for name, team := range ret.Teams { 2187 sort.Strings(team.Members) 2188 sort.Strings(team.Maintainers) 2189 sort.Strings(team.Previously) 2190 ret.Teams[name] = team 2191 } 2192 } 2193 2194 func TestOrgInvitations(t *testing.T) { 2195 cases := []struct { 2196 name string 2197 opt options 2198 invitees sets.Set[string] // overrides 2199 expected sets.Set[string] 2200 err bool 2201 }{ 2202 { 2203 name: "do not call on empty options", 2204 invitees: sets.New[string]("him", "her", "them"), 2205 expected: sets.Set[string]{}, 2206 }, 2207 { 2208 name: "call if fixOrgMembers", 2209 opt: options{ 2210 fixOrgMembers: true, 2211 }, 2212 invitees: sets.New[string]("him", "her", "them"), 2213 expected: sets.New[string]("him", "her", "them"), 2214 }, 2215 { 2216 name: "call if fixTeamMembers", 2217 opt: options{ 2218 fixTeamMembers: true, 2219 }, 2220 invitees: sets.New[string]("him", "her", "them"), 2221 expected: sets.New[string]("him", "her", "them"), 2222 }, 2223 { 2224 name: "ensure case normalization", 2225 opt: options{ 2226 fixOrgMembers: true, 2227 fixTeamMembers: true, 2228 }, 2229 invitees: sets.New[string]("MiXeD", "lower", "UPPER"), 2230 expected: sets.New[string]("mixed", "lower", "upper"), 2231 }, 2232 { 2233 name: "error if list fails", 2234 opt: options{ 2235 fixTeamMembers: true, 2236 fixOrgMembers: true, 2237 }, 2238 invitees: sets.New[string]("erick", "fail"), 2239 err: true, 2240 }, 2241 } 2242 2243 for _, tc := range cases { 2244 t.Run(tc.name, func(t *testing.T) { 2245 fc := &fakeClient{ 2246 invitees: tc.invitees, 2247 } 2248 actual, err := orgInvitations(tc.opt, fc, "random-org") 2249 switch { 2250 case err != nil: 2251 if !tc.err { 2252 t.Errorf("unexpected error: %v", err) 2253 } 2254 case tc.err: 2255 t.Errorf("failed to receive an error") 2256 case !reflect.DeepEqual(actual, tc.expected): 2257 t.Errorf("%#v != expected %#v", actual, tc.expected) 2258 } 2259 }) 2260 } 2261 } 2262 2263 type fakeTeamRepoClient struct { 2264 repos map[string][]github.Repo 2265 failList, failUpdate, failRemove bool 2266 } 2267 2268 func (c *fakeTeamRepoClient) ListTeamReposBySlug(org, teamSlug string) ([]github.Repo, error) { 2269 if c.failList { 2270 return nil, errors.New("injected failure to ListTeamRepos") 2271 } 2272 return c.repos[teamSlug], nil 2273 } 2274 2275 func (c *fakeTeamRepoClient) UpdateTeamRepoBySlug(org, teamSlug, repo string, permission github.TeamPermission) error { 2276 if c.failUpdate { 2277 return errors.New("injected failure to UpdateTeamRepos") 2278 } 2279 2280 permissions := github.PermissionsFromTeamPermission(permission) 2281 updated := false 2282 for i, repository := range c.repos[teamSlug] { 2283 if repository.Name == repo { 2284 c.repos[teamSlug][i].Permissions = permissions 2285 updated = true 2286 break 2287 } 2288 } 2289 2290 if !updated { 2291 c.repos[teamSlug] = append(c.repos[teamSlug], github.Repo{Name: repo, Permissions: permissions}) 2292 } 2293 2294 return nil 2295 } 2296 2297 func (c *fakeTeamRepoClient) RemoveTeamRepoBySlug(org, teamSlug, repo string) error { 2298 if c.failRemove { 2299 return errors.New("injected failure to RemoveTeamRepos") 2300 } 2301 2302 for i, repository := range c.repos[teamSlug] { 2303 if repository.Name == repo { 2304 c.repos[teamSlug] = append(c.repos[teamSlug][:i], c.repos[teamSlug][i+1:]...) 2305 break 2306 } 2307 } 2308 2309 return nil 2310 } 2311 2312 func TestConfigureTeamRepos(t *testing.T) { 2313 var testCases = []struct { 2314 name string 2315 githubTeams map[string]github.Team 2316 teamName string 2317 team org.Team 2318 existingRepos map[string][]github.Repo 2319 failList bool 2320 failUpdate bool 2321 failRemove bool 2322 expected map[string][]github.Repo 2323 expectedErr bool 2324 }{ 2325 { 2326 name: "githubTeams cache not containing team errors", 2327 githubTeams: map[string]github.Team{}, 2328 teamName: "team", 2329 expectedErr: true, 2330 }, 2331 { 2332 name: "listing repos failing errors", 2333 githubTeams: map[string]github.Team{"team": {ID: 1, Slug: "team"}}, 2334 teamName: "team", 2335 failList: true, 2336 expectedErr: true, 2337 }, 2338 { 2339 name: "nothing to do", 2340 githubTeams: map[string]github.Team{"team": {ID: 1, Slug: "team"}}, 2341 teamName: "team", 2342 team: org.Team{ 2343 Repos: map[string]github.RepoPermissionLevel{ 2344 "read": github.Read, 2345 "triage": github.Triage, 2346 "write": github.Write, 2347 "maintain": github.Maintain, 2348 "admin": github.Admin, 2349 }, 2350 }, 2351 existingRepos: map[string][]github.Repo{"team": { 2352 {Name: "read", Permissions: github.RepoPermissions{Pull: true}}, 2353 {Name: "triage", Permissions: github.RepoPermissions{Pull: true, Triage: true}}, 2354 {Name: "write", Permissions: github.RepoPermissions{Pull: true, Triage: true, Push: true}}, 2355 {Name: "maintain", Permissions: github.RepoPermissions{Pull: true, Triage: true, Push: true, Maintain: true}}, 2356 {Name: "admin", Permissions: github.RepoPermissions{Pull: true, Triage: true, Push: true, Maintain: true, Admin: true}}, 2357 }}, 2358 expected: map[string][]github.Repo{"team": { 2359 {Name: "read", Permissions: github.RepoPermissions{Pull: true}}, 2360 {Name: "triage", Permissions: github.RepoPermissions{Pull: true, Triage: true}}, 2361 {Name: "write", Permissions: github.RepoPermissions{Pull: true, Triage: true, Push: true}}, 2362 {Name: "maintain", Permissions: github.RepoPermissions{Pull: true, Triage: true, Push: true, Maintain: true}}, 2363 {Name: "admin", Permissions: github.RepoPermissions{Pull: true, Triage: true, Push: true, Maintain: true, Admin: true}}, 2364 }}, 2365 }, 2366 { 2367 name: "new requirement in org config gets added", 2368 githubTeams: map[string]github.Team{"team": {ID: 1, Slug: "team"}}, 2369 teamName: "team", 2370 team: org.Team{ 2371 Repos: map[string]github.RepoPermissionLevel{ 2372 "read": github.Read, 2373 "write": github.Write, 2374 "admin": github.Admin, 2375 "other-admin": github.Admin, 2376 }, 2377 }, 2378 existingRepos: map[string][]github.Repo{"team": { 2379 {Name: "read", Permissions: github.RepoPermissions{Pull: true}}, 2380 {Name: "write", Permissions: github.RepoPermissions{Pull: true, Triage: true, Push: true}}, 2381 {Name: "admin", Permissions: github.RepoPermissions{Pull: true, Triage: true, Push: true, Maintain: true, Admin: true}}, 2382 }}, 2383 expected: map[string][]github.Repo{"team": { 2384 {Name: "read", Permissions: github.RepoPermissions{Pull: true}}, 2385 {Name: "write", Permissions: github.RepoPermissions{Pull: true, Triage: true, Push: true}}, 2386 {Name: "admin", Permissions: github.RepoPermissions{Pull: true, Triage: true, Push: true, Maintain: true, Admin: true}}, 2387 {Name: "other-admin", Permissions: github.RepoPermissions{Pull: true, Triage: true, Push: true, Maintain: true, Admin: true}}, 2388 }}, 2389 }, 2390 { 2391 name: "change in permission on existing gets updated", 2392 githubTeams: map[string]github.Team{"team": {ID: 1, Slug: "team"}}, 2393 teamName: "team", 2394 team: org.Team{ 2395 Repos: map[string]github.RepoPermissionLevel{ 2396 "read": github.Read, 2397 "write": github.Write, 2398 "admin": github.Read, 2399 }, 2400 }, 2401 existingRepos: map[string][]github.Repo{"team": { 2402 {Name: "read", Permissions: github.RepoPermissions{Pull: true}}, 2403 {Name: "write", Permissions: github.RepoPermissions{Pull: true, Triage: true, Push: true}}, 2404 {Name: "admin", Permissions: github.RepoPermissions{Pull: true, Triage: true, Push: true, Maintain: true, Admin: true}}, 2405 }}, 2406 expected: map[string][]github.Repo{"team": { 2407 {Name: "read", Permissions: github.RepoPermissions{Pull: true}}, 2408 {Name: "write", Permissions: github.RepoPermissions{Pull: true, Triage: true, Push: true}}, 2409 {Name: "admin", Permissions: github.RepoPermissions{Pull: true}}, 2410 }}, 2411 }, 2412 { 2413 name: "omitted requirement gets removed", 2414 githubTeams: map[string]github.Team{"team": {ID: 1, Slug: "team"}}, 2415 teamName: "team", 2416 team: org.Team{ 2417 Repos: map[string]github.RepoPermissionLevel{ 2418 "write": github.Write, 2419 "admin": github.Read, 2420 }, 2421 }, 2422 existingRepos: map[string][]github.Repo{"team": { 2423 {Name: "read", Permissions: github.RepoPermissions{Pull: true}}, 2424 {Name: "write", Permissions: github.RepoPermissions{Pull: true, Triage: true, Push: true}}, 2425 {Name: "admin", Permissions: github.RepoPermissions{Pull: true, Triage: true, Push: true, Maintain: true, Admin: true}}, 2426 }}, 2427 expected: map[string][]github.Repo{"team": { 2428 {Name: "write", Permissions: github.RepoPermissions{Pull: true, Triage: true, Push: true}}, 2429 {Name: "admin", Permissions: github.RepoPermissions{Pull: true}}, 2430 }}, 2431 }, 2432 { 2433 name: "failed update errors", 2434 failUpdate: true, 2435 githubTeams: map[string]github.Team{"team": {ID: 1}}, 2436 teamName: "team", 2437 team: org.Team{ 2438 Repos: map[string]github.RepoPermissionLevel{ 2439 "will-fail": github.Write, 2440 }, 2441 }, 2442 existingRepos: map[string][]github.Repo{"some-team": {}}, 2443 expected: map[string][]github.Repo{"some-team": {}}, 2444 expectedErr: true, 2445 }, 2446 { 2447 name: "failed delete errors", 2448 failRemove: true, 2449 githubTeams: map[string]github.Team{"team": {ID: 1, Slug: "team"}}, 2450 teamName: "team", 2451 team: org.Team{ 2452 Repos: map[string]github.RepoPermissionLevel{}, 2453 }, 2454 existingRepos: map[string][]github.Repo{"team": { 2455 {Name: "needs-deletion", Permissions: github.RepoPermissions{Pull: true}}, 2456 }}, 2457 expected: map[string][]github.Repo{"team": { 2458 {Name: "needs-deletion", Permissions: github.RepoPermissions{Pull: true}}, 2459 }}, 2460 expectedErr: true, 2461 }, 2462 { 2463 name: "new requirement in child team config gets added", 2464 githubTeams: map[string]github.Team{"team": {ID: 1, Slug: "team"}, "child": {ID: 2, Slug: "child"}}, 2465 teamName: "team", 2466 team: org.Team{ 2467 Children: map[string]org.Team{ 2468 "child": { 2469 Repos: map[string]github.RepoPermissionLevel{ 2470 "read": github.Read, 2471 "write": github.Write, 2472 "admin": github.Admin, 2473 "other-admin": github.Admin, 2474 }, 2475 }, 2476 }, 2477 }, 2478 existingRepos: map[string][]github.Repo{"child": { 2479 {Name: "read", Permissions: github.RepoPermissions{Pull: true}}, 2480 {Name: "write", Permissions: github.RepoPermissions{Pull: true, Triage: true, Push: true}}, 2481 {Name: "admin", Permissions: github.RepoPermissions{Pull: true, Triage: true, Push: true, Maintain: true, Admin: true}}, 2482 }}, 2483 expected: map[string][]github.Repo{"child": { 2484 {Name: "read", Permissions: github.RepoPermissions{Pull: true}}, 2485 {Name: "write", Permissions: github.RepoPermissions{Pull: true, Triage: true, Push: true}}, 2486 {Name: "admin", Permissions: github.RepoPermissions{Pull: true, Triage: true, Push: true, Maintain: true, Admin: true}}, 2487 {Name: "other-admin", Permissions: github.RepoPermissions{Pull: true, Triage: true, Push: true, Maintain: true, Admin: true}}, 2488 }}, 2489 }, 2490 { 2491 name: "failure in a child errors", 2492 failRemove: true, 2493 githubTeams: map[string]github.Team{"team": {ID: 1, Slug: "team"}, "child": {ID: 2, Slug: "child"}}, 2494 teamName: "team", 2495 team: org.Team{ 2496 Repos: map[string]github.RepoPermissionLevel{}, 2497 Children: map[string]org.Team{ 2498 "child": { 2499 Repos: map[string]github.RepoPermissionLevel{}, 2500 }, 2501 }, 2502 }, 2503 existingRepos: map[string][]github.Repo{"child": { 2504 {Name: "needs-deletion", Permissions: github.RepoPermissions{Pull: true}}, 2505 }}, 2506 expected: map[string][]github.Repo{"child": { 2507 {Name: "needs-deletion", Permissions: github.RepoPermissions{Pull: true}}, 2508 }}, 2509 expectedErr: true, 2510 }, 2511 } 2512 2513 for _, testCase := range testCases { 2514 client := fakeTeamRepoClient{ 2515 repos: testCase.existingRepos, 2516 failList: testCase.failList, 2517 failUpdate: testCase.failUpdate, 2518 failRemove: testCase.failRemove, 2519 } 2520 err := configureTeamRepos(&client, testCase.githubTeams, testCase.teamName, "org", testCase.team) 2521 if err == nil && testCase.expectedErr { 2522 t.Errorf("%s: expected an error but got none", testCase.name) 2523 } 2524 if err != nil && !testCase.expectedErr { 2525 t.Errorf("%s: expected no error but got one: %v", testCase.name, err) 2526 } 2527 if diff := cmp.Diff(client.repos, testCase.expected); diff != "" { 2528 t.Errorf("%s: got incorrect team repos: %s", testCase.name, diff) 2529 } 2530 } 2531 } 2532 2533 type fakeRepoClient struct { 2534 t *testing.T 2535 repos map[string]github.FullRepo 2536 } 2537 2538 func (f fakeRepoClient) GetRepo(owner, name string) (github.FullRepo, error) { 2539 repo, ok := f.repos[name] 2540 if !ok { 2541 return repo, fmt.Errorf("repo not found") 2542 } 2543 return repo, nil 2544 } 2545 2546 func (f fakeRepoClient) GetRepos(orgName string, isUser bool) ([]github.Repo, error) { 2547 if orgName == "fail" { 2548 return nil, fmt.Errorf("injected GetRepos failure") 2549 } 2550 2551 repos := make([]github.Repo, 0, len(f.repos)) 2552 for _, repo := range f.repos { 2553 repos = append(repos, repo.Repo) 2554 } 2555 2556 // sort for deterministic output 2557 sort.Slice(repos, func(i, j int) bool { 2558 return repos[i].Name < repos[j].Name 2559 }) 2560 2561 return repos, nil 2562 } 2563 2564 func (f fakeRepoClient) CreateRepo(owner string, isUser bool, repoReq github.RepoCreateRequest) (*github.FullRepo, error) { 2565 if *repoReq.Name == "fail" { 2566 return nil, fmt.Errorf("injected CreateRepo failure") 2567 } 2568 2569 if _, hasRepo := f.repos[*repoReq.Name]; hasRepo { 2570 f.t.Errorf("CreateRepo() called on repo that already exists") 2571 return nil, fmt.Errorf("CreateRepo() called on repo that already exists") 2572 } 2573 2574 repo := repoReq.ToRepo() 2575 f.repos[*repoReq.Name] = *repo 2576 2577 return repo, nil 2578 } 2579 2580 func (f fakeRepoClient) UpdateRepo(owner, name string, want github.RepoUpdateRequest) (*github.FullRepo, error) { 2581 if name == "fail" { 2582 return nil, fmt.Errorf("injected UpdateRepo failure") 2583 } 2584 if want.Archived != nil && !*want.Archived { 2585 f.t.Errorf("UpdateRepo() called to unarchive a repo (not supported by API)") 2586 return nil, fmt.Errorf("UpdateRepo() called to unarchive a repo (not supported by API)") 2587 } 2588 2589 have, exists := f.repos[name] 2590 if !exists { 2591 f.t.Errorf("UpdateRepo() called on repo that does not exists") 2592 return nil, fmt.Errorf("UpdateRepo() called on repo that does not exist") 2593 } 2594 2595 if have.Archived { 2596 return nil, fmt.Errorf("Repository was archived so is read-only.") 2597 } 2598 2599 updateString := func(have, want *string) { 2600 if want != nil { 2601 *have = *want 2602 } 2603 } 2604 2605 updateBool := func(have, want *bool) { 2606 if want != nil { 2607 *have = *want 2608 } 2609 } 2610 2611 updateString(&have.Name, want.Name) 2612 updateString(&have.DefaultBranch, want.DefaultBranch) 2613 updateString(&have.Homepage, want.Homepage) 2614 updateString(&have.Description, want.Description) 2615 updateBool(&have.Archived, want.Archived) 2616 updateBool(&have.Private, want.Private) 2617 updateBool(&have.HasIssues, want.HasIssues) 2618 updateBool(&have.HasProjects, want.HasProjects) 2619 updateBool(&have.HasWiki, want.HasWiki) 2620 updateBool(&have.AllowSquashMerge, want.AllowSquashMerge) 2621 updateBool(&have.AllowMergeCommit, want.AllowMergeCommit) 2622 updateBool(&have.AllowRebaseMerge, want.AllowRebaseMerge) 2623 updateString(&have.SquashMergeCommitTitle, want.SquashMergeCommitTitle) 2624 updateString(&have.SquashMergeCommitMessage, want.SquashMergeCommitMessage) 2625 2626 f.repos[name] = have 2627 return &have, nil 2628 } 2629 2630 func makeFakeRepoClient(t *testing.T, repos ...github.FullRepo) fakeRepoClient { 2631 fc := fakeRepoClient{ 2632 repos: make(map[string]github.FullRepo, len(repos)), 2633 t: t, 2634 } 2635 for _, repo := range repos { 2636 fc.repos[repo.Name] = repo 2637 } 2638 2639 return fc 2640 } 2641 2642 func TestConfigureRepos(t *testing.T) { 2643 orgName := "test-org" 2644 isOrg := false 2645 no := false 2646 yes := true 2647 updated := "UPDATED" 2648 2649 oldName := "old" 2650 oldRepo := github.Repo{ 2651 Name: oldName, 2652 FullName: fmt.Sprintf("%s/%s", orgName, oldName), 2653 Description: "An old existing repository", 2654 } 2655 2656 newName := "new" 2657 newDescription := "A new repository." 2658 newConfigRepo := org.Repo{ 2659 Description: &newDescription, 2660 } 2661 newRepo := github.Repo{ 2662 Name: newName, 2663 Description: newDescription, 2664 } 2665 2666 fail := "fail" 2667 failRepo := github.Repo{ 2668 Name: fail, 2669 } 2670 2671 testCases := []struct { 2672 description string 2673 opts options 2674 orgConfig org.Config 2675 orgNameOverride string 2676 repos []github.FullRepo 2677 2678 expectError bool 2679 expectedRepos []github.Repo 2680 }{ 2681 { 2682 description: "survives empty config", 2683 expectedRepos: []github.Repo{}, 2684 }, 2685 { 2686 description: "survives nil repos config", 2687 orgConfig: org.Config{ 2688 Repos: nil, 2689 }, 2690 expectedRepos: []github.Repo{}, 2691 }, 2692 { 2693 description: "survives empty repos config", 2694 orgConfig: org.Config{ 2695 Repos: map[string]org.Repo{}, 2696 }, 2697 expectedRepos: []github.Repo{}, 2698 }, 2699 { 2700 description: "nonexistent repo is created", 2701 orgConfig: org.Config{ 2702 Repos: map[string]org.Repo{ 2703 newName: newConfigRepo, 2704 }, 2705 }, 2706 repos: []github.FullRepo{{Repo: oldRepo}}, 2707 2708 expectedRepos: []github.Repo{newRepo, oldRepo}, 2709 }, 2710 { 2711 description: "GetRepos failure is propagated", 2712 orgNameOverride: "fail", 2713 orgConfig: org.Config{ 2714 Repos: map[string]org.Repo{ 2715 newName: newConfigRepo, 2716 }, 2717 }, 2718 repos: []github.FullRepo{{Repo: oldRepo}}, 2719 2720 expectError: true, 2721 expectedRepos: []github.Repo{oldRepo}, 2722 }, 2723 { 2724 description: "CreateRepo failure is propagated", 2725 orgConfig: org.Config{ 2726 Repos: map[string]org.Repo{ 2727 fail: newConfigRepo, 2728 }, 2729 }, 2730 repos: []github.FullRepo{{Repo: oldRepo}}, 2731 2732 expectError: true, 2733 expectedRepos: []github.Repo{oldRepo}, 2734 }, 2735 { 2736 description: "duplicate repo names different only by case are detected", 2737 orgConfig: org.Config{ 2738 Repos: map[string]org.Repo{ 2739 "repo": newConfigRepo, 2740 "REPO": newConfigRepo, 2741 }, 2742 }, 2743 repos: []github.FullRepo{{Repo: oldRepo}}, 2744 2745 expectError: true, 2746 expectedRepos: []github.Repo{oldRepo}, 2747 }, 2748 { 2749 description: "existing repo is updated", 2750 orgConfig: org.Config{ 2751 Repos: map[string]org.Repo{ 2752 oldName: newConfigRepo, 2753 }, 2754 }, 2755 repos: []github.FullRepo{{Repo: oldRepo}}, 2756 expectedRepos: []github.Repo{ 2757 { 2758 Name: oldName, 2759 Description: newDescription, 2760 FullName: fmt.Sprintf("%s/%s", orgName, oldName), 2761 }, 2762 }, 2763 }, 2764 { 2765 description: "UpdateRepo failure is propagated", 2766 orgConfig: org.Config{ 2767 Repos: map[string]org.Repo{ 2768 "fail": newConfigRepo, 2769 }, 2770 }, 2771 repos: []github.FullRepo{{Repo: failRepo}}, 2772 expectError: true, 2773 expectedRepos: []github.Repo{failRepo}, 2774 }, 2775 { 2776 // https://developer.github.com/v3/repos/#edit 2777 // "Note: You cannot unarchive repositories through the API." 2778 // Archived repositories are read-only, and updates fail with 403: 2779 // "Repository was archived so is read-only." 2780 description: "request to unarchive a repo fails, repo is read-only", 2781 orgConfig: org.Config{ 2782 Repos: map[string]org.Repo{ 2783 oldName: {Archived: &no, Description: &updated}, 2784 }, 2785 }, 2786 repos: []github.FullRepo{{Repo: github.Repo{Name: oldName, Archived: true, Description: "OLD"}}}, 2787 expectError: true, 2788 expectedRepos: []github.Repo{{Name: oldName, Archived: true, Description: "OLD"}}, 2789 }, 2790 { 2791 // https://developer.github.com/v3/repos/#edit 2792 // "Note: You cannot unarchive repositories through the API." 2793 // Archived repositories are read-only, and updates fail with 403: 2794 // "Repository was archived so is read-only." 2795 description: "no field changes on archived repo", 2796 orgConfig: org.Config{ 2797 Repos: map[string]org.Repo{ 2798 oldName: {Archived: &yes, Description: &updated}, 2799 }, 2800 }, 2801 repos: []github.FullRepo{{Repo: github.Repo{Name: oldName, Archived: true, Description: "OLD"}}}, 2802 expectError: false, 2803 expectedRepos: []github.Repo{{Name: oldName, Archived: true, Description: "OLD"}}, 2804 }, 2805 { 2806 description: "request to archive repo fails when not allowed, but updates other fields", 2807 orgConfig: org.Config{ 2808 Repos: map[string]org.Repo{ 2809 oldName: {Archived: &yes, Description: &updated}, 2810 }, 2811 }, 2812 repos: []github.FullRepo{{Repo: github.Repo{Name: oldName, Archived: false, Description: "OLD"}}}, 2813 expectError: true, 2814 expectedRepos: []github.Repo{{Name: oldName, Archived: false, Description: updated}}, 2815 }, 2816 { 2817 description: "request to archive repo succeeds when allowed", 2818 opts: options{ 2819 allowRepoArchival: true, 2820 }, 2821 orgConfig: org.Config{ 2822 Repos: map[string]org.Repo{ 2823 oldName: {Archived: &yes}, 2824 }, 2825 }, 2826 repos: []github.FullRepo{{Repo: github.Repo{Name: oldName, Archived: false}}}, 2827 expectedRepos: []github.Repo{{Name: oldName, Archived: true}}, 2828 }, 2829 { 2830 description: "request to publish a private repo fails when not allowed, but updates other fields", 2831 orgConfig: org.Config{ 2832 Repos: map[string]org.Repo{ 2833 oldName: {Private: &no, Description: &updated}, 2834 }, 2835 }, 2836 repos: []github.FullRepo{{Repo: github.Repo{Name: oldName, Private: true, Description: "OLD"}}}, 2837 expectError: true, 2838 expectedRepos: []github.Repo{{Name: oldName, Private: true, Description: updated}}, 2839 }, 2840 { 2841 description: "request to publish a private repo succeeds when allowed", 2842 opts: options{ 2843 allowRepoPublish: true, 2844 }, 2845 orgConfig: org.Config{ 2846 Repos: map[string]org.Repo{ 2847 oldName: {Private: &no}, 2848 }, 2849 }, 2850 repos: []github.FullRepo{{Repo: github.Repo{Name: oldName, Private: true}}}, 2851 expectedRepos: []github.Repo{{Name: oldName, Private: false}}, 2852 }, 2853 { 2854 description: "renaming a repo is successful", 2855 orgConfig: org.Config{ 2856 Repos: map[string]org.Repo{ 2857 newName: {Previously: []string{oldName}}, 2858 }, 2859 }, 2860 repos: []github.FullRepo{{Repo: github.Repo{Name: oldName, Description: "renamed repo"}}}, 2861 expectedRepos: []github.Repo{{Name: newName, Description: "renamed repo"}}, 2862 }, 2863 { 2864 description: "renaming a repo by just changing case is successful", 2865 orgConfig: org.Config{ 2866 Repos: map[string]org.Repo{ 2867 "repo": {Previously: []string{"REPO"}}, 2868 }, 2869 }, 2870 repos: []github.FullRepo{{Repo: github.Repo{Name: "REPO", Description: "renamed repo"}}}, 2871 expectedRepos: []github.Repo{{Name: "repo", Description: "renamed repo"}}, 2872 }, 2873 { 2874 description: "dup between a repo name and a previous name is detected", 2875 orgConfig: org.Config{ 2876 Repos: map[string]org.Repo{ 2877 newName: {Previously: []string{oldName}}, 2878 oldName: {Description: &newDescription}, 2879 }, 2880 }, 2881 repos: []github.FullRepo{{Repo: github.Repo{Name: oldName, Description: "this repo shall not be touched"}}}, 2882 expectError: true, 2883 expectedRepos: []github.Repo{{Name: oldName, Description: "this repo shall not be touched"}}, 2884 }, 2885 { 2886 description: "dup between two previous names is detected", 2887 orgConfig: org.Config{ 2888 Repos: map[string]org.Repo{ 2889 "wants-projects": {Previously: []string{oldName}, HasProjects: &yes, HasWiki: &no}, 2890 "wants-wiki": {Previously: []string{oldName}, HasProjects: &no, HasWiki: &yes}, 2891 }, 2892 }, 2893 repos: []github.FullRepo{{Repo: github.Repo{Name: oldName, Description: "this repo shall not be touched"}}}, 2894 expectError: true, 2895 expectedRepos: []github.Repo{{Name: oldName, Description: "this repo shall not be touched"}}, 2896 }, 2897 { 2898 description: "error detected when both a repo and a repo of its previous name exist", 2899 orgConfig: org.Config{ 2900 Repos: map[string]org.Repo{ 2901 newName: {Previously: []string{oldName}, Description: &newDescription}, 2902 }, 2903 }, 2904 repos: []github.FullRepo{ 2905 {Repo: github.Repo{Name: oldName, Description: "this repo shall not be touched"}}, 2906 {Repo: github.Repo{Name: newName, Description: "this repo shall not be touched too"}}, 2907 }, 2908 expectError: true, 2909 expectedRepos: []github.Repo{ 2910 {Name: newName, Description: "this repo shall not be touched too"}, 2911 {Name: oldName, Description: "this repo shall not be touched"}, 2912 }, 2913 }, 2914 { 2915 description: "error detected when multiple previous repos exist", 2916 orgConfig: org.Config{ 2917 Repos: map[string]org.Repo{ 2918 newName: {Previously: []string{oldName, "even-older"}, Description: &newDescription}, 2919 }, 2920 }, 2921 repos: []github.FullRepo{ 2922 {Repo: github.Repo{Name: oldName, Description: "this repo shall not be touched"}}, 2923 {Repo: github.Repo{Name: "even-older", Description: "this repo shall not be touched too"}}, 2924 }, 2925 expectError: true, 2926 expectedRepos: []github.Repo{ 2927 {Name: "even-older", Description: "this repo shall not be touched too"}, 2928 {Name: oldName, Description: "this repo shall not be touched"}, 2929 }, 2930 }, 2931 { 2932 description: "repos are renamed to defined case even without explicit `previously` field", 2933 orgConfig: org.Config{ 2934 Repos: map[string]org.Repo{ 2935 "CamelCase": {Description: &newDescription}, 2936 }, 2937 }, 2938 repos: []github.FullRepo{{Repo: github.Repo{Name: "CAMELCASE", Description: newDescription}}}, 2939 expectedRepos: []github.Repo{{Name: "CamelCase", Description: newDescription}}, 2940 }, 2941 { 2942 description: "avoid creating archived repo", 2943 orgConfig: org.Config{ 2944 Repos: map[string]org.Repo{ 2945 oldName: {Archived: &yes}, 2946 }, 2947 }, 2948 repos: []github.FullRepo{}, 2949 expectError: true, 2950 expectedRepos: []github.Repo{}, 2951 }, 2952 } 2953 for _, tc := range testCases { 2954 t.Run(tc.description, func(t *testing.T) { 2955 fc := makeFakeRepoClient(t, tc.repos...) 2956 var err error 2957 if len(tc.orgNameOverride) > 0 { 2958 err = configureRepos(tc.opts, fc, tc.orgNameOverride, tc.orgConfig) 2959 } else { 2960 err = configureRepos(tc.opts, fc, orgName, tc.orgConfig) 2961 } 2962 if err != nil && !tc.expectError { 2963 t.Errorf("%s: unexpected error: %v", tc.description, err) 2964 } 2965 if err == nil && tc.expectError { 2966 t.Errorf("%s: expected error, got none", tc.description) 2967 } 2968 2969 reposAfter, err := fc.GetRepos(orgName, isOrg) 2970 if err != nil { 2971 t.Fatalf("%s: unexpected GetRepos error: %v", tc.description, err) 2972 } 2973 if !reflect.DeepEqual(reposAfter, tc.expectedRepos) { 2974 t.Errorf("%s: unexpected repos after configureRepos():\n%s", tc.description, cmp.Diff(reposAfter, tc.expectedRepos)) 2975 } 2976 }) 2977 } 2978 } 2979 2980 func TestValidateRepos(t *testing.T) { 2981 description := "cool repo" 2982 testCases := []struct { 2983 description string 2984 config map[string]org.Repo 2985 expectError bool 2986 }{ 2987 { 2988 description: "handles nil map", 2989 }, 2990 { 2991 description: "handles empty map", 2992 config: map[string]org.Repo{}, 2993 }, 2994 { 2995 description: "handles valid config", 2996 config: map[string]org.Repo{ 2997 "repo": {Description: &description}, 2998 }, 2999 }, 3000 { 3001 description: "finds repo names duplicate when normalized", 3002 config: map[string]org.Repo{ 3003 "repo": {Description: &description}, 3004 "Repo": {Description: &description}, 3005 }, 3006 expectError: true, 3007 }, 3008 { 3009 description: "finds name confict between previous and current names", 3010 config: map[string]org.Repo{ 3011 "repo": {Previously: []string{"conflict"}}, 3012 "conflict": {Description: &description}, 3013 }, 3014 expectError: true, 3015 }, 3016 { 3017 description: "finds name confict between two previous names", 3018 config: map[string]org.Repo{ 3019 "repo": {Previously: []string{"conflict"}}, 3020 "another-repo": {Previously: []string{"conflict"}}, 3021 }, 3022 expectError: true, 3023 }, 3024 { 3025 description: "allows case-duplicate name between former and current name", 3026 config: map[string]org.Repo{ 3027 "repo": {Previously: []string{"REPO"}}, 3028 }, 3029 }, 3030 } 3031 3032 for _, tc := range testCases { 3033 t.Run(tc.description, func(t *testing.T) { 3034 err := validateRepos(tc.config) 3035 if err == nil && tc.expectError { 3036 t.Errorf("%s: expected error, got none", tc.description) 3037 } else if err != nil && !tc.expectError { 3038 t.Errorf("%s: unexpected error: %v", tc.description, err) 3039 } 3040 }) 3041 } 3042 } 3043 3044 func TestNewRepoUpdateRequest(t *testing.T) { 3045 repoName := "repo-name" 3046 newRepoName := "renamed-repo" 3047 description := "description of repo-name" 3048 homepage := "https://somewhe.re" 3049 master := "master" 3050 branch := "branch" 3051 squashMergeCommitTitle := "PR_TITLE" 3052 squashMergeCommitMessage := "COMMIT_MESSAGES" 3053 3054 testCases := []struct { 3055 description string 3056 current github.FullRepo 3057 name string 3058 newState org.Repo 3059 3060 expected github.RepoUpdateRequest 3061 }{ 3062 { 3063 description: "update is just a delta from current state", 3064 current: github.FullRepo{ 3065 Repo: github.Repo{ 3066 Name: repoName, 3067 Description: description, 3068 Homepage: homepage, 3069 DefaultBranch: master, 3070 }, 3071 }, 3072 name: repoName, 3073 newState: org.Repo{ 3074 Description: &description, 3075 DefaultBranch: &branch, 3076 }, 3077 expected: github.RepoUpdateRequest{ 3078 DefaultBranch: &branch, 3079 }, 3080 }, 3081 { 3082 description: "empty delta is returned when no update is needed", 3083 current: github.FullRepo{Repo: github.Repo{ 3084 Name: repoName, 3085 Description: description, 3086 }}, 3087 name: repoName, 3088 newState: org.Repo{ 3089 Description: &description, 3090 }, 3091 }, 3092 { 3093 description: "request to rename a repo works", 3094 current: github.FullRepo{Repo: github.Repo{ 3095 Name: repoName, 3096 }}, 3097 name: newRepoName, 3098 newState: org.Repo{ 3099 Description: &description, 3100 }, 3101 expected: github.RepoUpdateRequest{ 3102 RepoRequest: github.RepoRequest{ 3103 Name: &newRepoName, 3104 Description: &description, 3105 }, 3106 }, 3107 }, 3108 { 3109 description: "request to update commit messages works", 3110 current: github.FullRepo{ 3111 Repo: github.Repo{ 3112 Name: repoName, 3113 }, 3114 SquashMergeCommitTitle: "COMMIT_MESSAGES", 3115 SquashMergeCommitMessage: "COMMIT_OR_PR_TITLE", 3116 }, 3117 name: newRepoName, 3118 newState: org.Repo{ 3119 Description: &description, 3120 SquashMergeCommitTitle: &squashMergeCommitTitle, 3121 SquashMergeCommitMessage: &squashMergeCommitMessage, 3122 }, 3123 expected: github.RepoUpdateRequest{ 3124 RepoRequest: github.RepoRequest{ 3125 Name: &newRepoName, 3126 Description: &description, 3127 SquashMergeCommitTitle: &squashMergeCommitTitle, 3128 SquashMergeCommitMessage: &squashMergeCommitMessage, 3129 }, 3130 }, 3131 }, 3132 } 3133 3134 for _, tc := range testCases { 3135 t.Run(tc.description, func(t *testing.T) { 3136 update := newRepoUpdateRequest(tc.current, tc.name, tc.newState) 3137 if !reflect.DeepEqual(tc.expected, update) { 3138 t.Errorf("%s: update request differs from expected:%s", tc.description, cmp.Diff(tc.expected, update)) 3139 } 3140 }) 3141 } 3142 }