github.com/munnerz/test-infra@v0.0.0-20190108210205-ce3d181dc989/prow/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 "k8s.io/test-infra/prow/config/org" 28 "k8s.io/test-infra/prow/flagutil" 29 "k8s.io/test-infra/prow/github" 30 31 "k8s.io/apimachinery/pkg/util/sets" 32 "sigs.k8s.io/yaml" 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: "maximal delta", 63 args: []string{"--config-path=foo", "--maximum-removal-delta=1"}, 64 expected: &options{ 65 config: "foo", 66 minAdmins: defaultMinAdmins, 67 requireSelf: true, 68 maximumDelta: 1, 69 tokensPerHour: defaultTokens, 70 tokenBurst: defaultBurst, 71 }, 72 }, 73 { 74 name: "minimal delta", 75 args: []string{"--config-path=foo", "--maximum-removal-delta=0"}, 76 expected: &options{ 77 config: "foo", 78 minAdmins: defaultMinAdmins, 79 requireSelf: true, 80 maximumDelta: 0, 81 tokensPerHour: defaultTokens, 82 tokenBurst: defaultBurst, 83 }, 84 }, 85 { 86 name: "minimal admins", 87 args: []string{"--config-path=foo", "--min-admins=2"}, 88 expected: &options{ 89 config: "foo", 90 minAdmins: 2, 91 requireSelf: true, 92 maximumDelta: defaultDelta, 93 tokensPerHour: defaultTokens, 94 tokenBurst: defaultBurst, 95 }, 96 }, 97 { 98 name: "reject burst > tokens", 99 args: []string{"--config-path=foo", "--tokens=10", "--token-burst=11"}, 100 }, 101 { 102 name: "reject dump and confirm", 103 args: []string{"--confirm", "--dump=frogger"}, 104 }, 105 { 106 name: "reject dump and config-path", 107 args: []string{"--config-path=foo", "--dump=frogger"}, 108 }, 109 { 110 name: "reject --fix-team-members without --fix-teams", 111 args: []string{"--config-path=foo", "--fix-team-members"}, 112 }, 113 { 114 name: "allow disabled throttle", 115 args: []string{"--config-path=foo", "--tokens=0"}, 116 expected: &options{ 117 config: "foo", 118 minAdmins: defaultMinAdmins, 119 requireSelf: true, 120 maximumDelta: defaultDelta, 121 tokensPerHour: 0, 122 tokenBurst: defaultBurst, 123 }, 124 }, 125 { 126 name: "allow dump without config", 127 args: []string{"--dump=frogger"}, 128 expected: &options{ 129 minAdmins: defaultMinAdmins, 130 requireSelf: true, 131 maximumDelta: defaultDelta, 132 tokensPerHour: defaultTokens, 133 tokenBurst: defaultBurst, 134 dump: "frogger", 135 }, 136 }, 137 { 138 name: "minimal", 139 args: []string{"--config-path=foo"}, 140 expected: &options{ 141 config: "foo", 142 minAdmins: defaultMinAdmins, 143 requireSelf: true, 144 maximumDelta: defaultDelta, 145 tokensPerHour: defaultTokens, 146 tokenBurst: defaultBurst, 147 }, 148 }, 149 { 150 name: "full", 151 args: []string{"--config-path=foo", "--github-token-path=bar", "--github-endpoint=weird://url", "--confirm=true", "--require-self=false", "--tokens=5", "--token-burst=2", "--dump=", "--fix-org", "--fix-org-members", "--fix-teams", "--fix-team-members"}, 152 expected: &options{ 153 config: "foo", 154 confirm: true, 155 requireSelf: false, 156 minAdmins: defaultMinAdmins, 157 maximumDelta: defaultDelta, 158 tokensPerHour: 5, 159 tokenBurst: 2, 160 fixOrg: true, 161 fixOrgMembers: true, 162 fixTeams: true, 163 fixTeamMembers: true, 164 }, 165 }, 166 } 167 168 for _, tc := range cases { 169 flags := flag.NewFlagSet(tc.name, flag.ContinueOnError) 170 var actual options 171 err := actual.parseArgs(flags, tc.args) 172 actual.github = flagutil.GitHubOptions{} 173 switch { 174 case err == nil && tc.expected == nil: 175 t.Errorf("%s: failed to return an error", tc.name) 176 case err != nil && tc.expected != nil: 177 t.Errorf("%s: unexpected error: %v", tc.name, err) 178 case tc.expected != nil && !reflect.DeepEqual(*tc.expected, actual): 179 t.Errorf("%s: actual %v != expected %v", tc.name, actual, *tc.expected) 180 } 181 } 182 } 183 184 type fakeClient struct { 185 orgMembers sets.String 186 admins sets.String 187 invitees sets.String 188 members sets.String 189 removed sets.String 190 newAdmins sets.String 191 newMembers sets.String 192 } 193 194 func (c *fakeClient) BotName() (string, error) { 195 return "me", nil 196 } 197 198 func (c fakeClient) makeMembers(people sets.String) []github.TeamMember { 199 var ret []github.TeamMember 200 for p := range people { 201 ret = append(ret, github.TeamMember{Login: p}) 202 } 203 return ret 204 } 205 206 func (c *fakeClient) ListOrgMembers(org, role string) ([]github.TeamMember, error) { 207 switch role { 208 case github.RoleMember: 209 return c.makeMembers(c.members), nil 210 case github.RoleAdmin: 211 return c.makeMembers(c.admins), nil 212 default: 213 // RoleAll: implement when/if necessary 214 return nil, fmt.Errorf("bad role: %s", role) 215 } 216 } 217 218 func (c *fakeClient) ListOrgInvitations(org string) ([]github.OrgInvitation, error) { 219 var ret []github.OrgInvitation 220 for p := range c.invitees { 221 if p == "fail" { 222 return nil, errors.New("injected list org invitations failure") 223 } 224 ret = append(ret, github.OrgInvitation{ 225 TeamMember: github.TeamMember{ 226 Login: p, 227 }, 228 }) 229 } 230 return ret, nil 231 } 232 233 func (c *fakeClient) RemoveOrgMembership(org, user string) error { 234 if user == "fail" { 235 return errors.New("injected remove org membership failure") 236 } 237 c.removed.Insert(user) 238 c.admins.Delete(user) 239 c.members.Delete(user) 240 return nil 241 } 242 243 func (c *fakeClient) UpdateOrgMembership(org, user string, admin bool) (*github.OrgMembership, error) { 244 if user == "fail" { 245 return nil, errors.New("injected update org failure") 246 } 247 var state string 248 if c.members.Has(user) || c.admins.Has(user) { 249 state = github.StateActive 250 } else { 251 state = github.StatePending 252 } 253 var role string 254 if admin { 255 c.newAdmins.Insert(user) 256 c.admins.Insert(user) 257 role = github.RoleAdmin 258 } else { 259 c.newMembers.Insert(user) 260 c.members.Insert(user) 261 role = github.RoleMember 262 } 263 return &github.OrgMembership{ 264 Membership: github.Membership{ 265 Role: role, 266 State: state, 267 }, 268 }, nil 269 } 270 271 func (c *fakeClient) ListTeamMembers(id int, role string) ([]github.TeamMember, error) { 272 if id != teamID { 273 return nil, fmt.Errorf("only team 66 supported, not %d", id) 274 } 275 switch role { 276 case github.RoleMember: 277 return c.makeMembers(c.members), nil 278 case github.RoleMaintainer: 279 return c.makeMembers(c.admins), nil 280 default: 281 return nil, fmt.Errorf("fake does not support: %v", role) 282 } 283 } 284 285 func (c *fakeClient) ListTeamInvitations(id int) ([]github.OrgInvitation, error) { 286 if id != teamID { 287 return nil, fmt.Errorf("only team 66 supported, not %d", id) 288 } 289 var ret []github.OrgInvitation 290 for p := range c.invitees { 291 if p == "fail" { 292 return nil, errors.New("injected list org invitations failure") 293 } 294 ret = append(ret, github.OrgInvitation{ 295 TeamMember: github.TeamMember{ 296 Login: p, 297 }, 298 }) 299 } 300 return ret, nil 301 } 302 303 const teamID = 66 304 305 func (c *fakeClient) UpdateTeamMembership(id int, user string, maintainer bool) (*github.TeamMembership, error) { 306 if id != teamID { 307 return nil, fmt.Errorf("only team %d supported, not %d", teamID, id) 308 } 309 if user == "fail" { 310 return nil, fmt.Errorf("injected failure for %s", user) 311 } 312 var state string 313 if c.orgMembers.Has(user) || len(c.orgMembers) == 0 { 314 state = github.StateActive 315 } else { 316 state = github.StatePending 317 } 318 var role string 319 if maintainer { 320 c.newAdmins.Insert(user) 321 c.admins.Insert(user) 322 role = github.RoleMaintainer 323 } else { 324 c.newMembers.Insert(user) 325 c.members.Insert(user) 326 role = github.RoleMember 327 } 328 return &github.TeamMembership{ 329 Membership: github.Membership{ 330 Role: role, 331 State: state, 332 }, 333 }, nil 334 } 335 336 func (c *fakeClient) RemoveTeamMembership(id int, user string) error { 337 if id != teamID { 338 return fmt.Errorf("only team %d supported, not %d", teamID, id) 339 } 340 if user == "fail" { 341 return fmt.Errorf("injected failure for %s", user) 342 } 343 c.removed.Insert(user) 344 c.admins.Delete(user) 345 c.members.Delete(user) 346 return nil 347 } 348 349 func TestConfigureMembers(t *testing.T) { 350 cases := []struct { 351 name string 352 want memberships 353 have memberships 354 remove sets.String 355 members sets.String 356 supers sets.String 357 invitees sets.String 358 err bool 359 }{ 360 { 361 name: "forgot to remove duplicate entry", 362 want: memberships{ 363 members: sets.NewString("me"), 364 super: sets.NewString("me"), 365 }, 366 err: true, 367 }, 368 { 369 name: "removal fails", 370 have: memberships{ 371 members: sets.NewString("fail"), 372 }, 373 err: true, 374 }, 375 { 376 name: "adding admin fails", 377 want: memberships{ 378 super: sets.NewString("fail"), 379 }, 380 err: true, 381 }, 382 { 383 name: "adding member fails", 384 want: memberships{ 385 members: sets.NewString("fail"), 386 }, 387 err: true, 388 }, 389 { 390 name: "promote to admin", 391 have: memberships{ 392 members: sets.NewString("promote"), 393 }, 394 want: memberships{ 395 super: sets.NewString("promote"), 396 }, 397 supers: sets.NewString("promote"), 398 }, 399 { 400 name: "downgrade to member", 401 have: memberships{ 402 super: sets.NewString("downgrade"), 403 }, 404 want: memberships{ 405 members: sets.NewString("downgrade"), 406 }, 407 members: sets.NewString("downgrade"), 408 }, 409 { 410 name: "some of everything", 411 have: memberships{ 412 super: sets.NewString("keep-admin", "drop-admin"), 413 members: sets.NewString("keep-member", "drop-member"), 414 }, 415 want: memberships{ 416 members: sets.NewString("keep-member", "new-member"), 417 super: sets.NewString("keep-admin", "new-admin"), 418 }, 419 remove: sets.NewString("drop-admin", "drop-member"), 420 members: sets.NewString("new-member"), 421 supers: sets.NewString("new-admin"), 422 }, 423 { 424 name: "ensure case insensitivity", 425 have: memberships{ 426 super: sets.NewString("lower"), 427 members: sets.NewString("UPPER"), 428 }, 429 want: memberships{ 430 super: sets.NewString("Lower"), 431 members: sets.NewString("UpPeR"), 432 }, 433 }, 434 { 435 name: "remove invites for those not in org config", 436 have: memberships{ 437 members: sets.NewString("member-one", "member-two"), 438 }, 439 want: memberships{ 440 members: sets.NewString("member-one", "member-two"), 441 }, 442 remove: sets.NewString("member-three"), 443 invitees: sets.NewString("member-three"), 444 }, 445 } 446 447 for _, tc := range cases { 448 t.Run(tc.name, func(t *testing.T) { 449 removed := sets.String{} 450 members := sets.String{} 451 supers := sets.String{} 452 adder := func(user string, super bool) error { 453 if user == "fail" { 454 return fmt.Errorf("injected adder failure for %s", user) 455 } 456 if super { 457 supers.Insert(user) 458 } else { 459 members.Insert(user) 460 } 461 return nil 462 } 463 464 remover := func(user string) error { 465 if user == "fail" { 466 return fmt.Errorf("injected remover failure for %s", user) 467 } 468 removed.Insert(user) 469 return nil 470 } 471 472 err := configureMembers(tc.have, tc.want, tc.invitees, adder, remover) 473 switch { 474 case err != nil: 475 if !tc.err { 476 t.Errorf("Unexpected error: %v", err) 477 } 478 case tc.err: 479 t.Errorf("Failed to receive error") 480 default: 481 if err := cmpLists(tc.remove.List(), removed.List()); err != nil { 482 t.Errorf("Wrong users removed: %v", err) 483 } else if err := cmpLists(tc.members.List(), members.List()); err != nil { 484 t.Errorf("Wrong members added: %v", err) 485 } else if err := cmpLists(tc.supers.List(), supers.List()); err != nil { 486 t.Errorf("Wrong supers added: %v", err) 487 } 488 } 489 }) 490 } 491 } 492 493 func TestConfigureOrgMembers(t *testing.T) { 494 cases := []struct { 495 name string 496 opt options 497 config org.Config 498 admins []string 499 members []string 500 invitations []string 501 err bool 502 remove []string 503 addAdmins []string 504 addMembers []string 505 }{ 506 { 507 name: "too few admins", 508 opt: options{ 509 minAdmins: 5, 510 }, 511 config: org.Config{ 512 Admins: []string{"joe"}, 513 }, 514 err: true, 515 }, 516 { 517 name: "remove too many admins", 518 opt: options{ 519 maximumDelta: 0.3, 520 }, 521 config: org.Config{ 522 Admins: []string{"keep", "me"}, 523 }, 524 admins: []string{"a", "b", "c", "keep"}, 525 err: true, 526 }, 527 { 528 name: "forgot to add self", 529 opt: options{ 530 requireSelf: true, 531 }, 532 config: org.Config{ 533 Admins: []string{"other"}, 534 }, 535 err: true, 536 }, 537 { 538 name: "forgot to add required admins", 539 opt: options{ 540 requiredAdmins: flagutil.NewStrings("francis"), 541 }, 542 err: true, 543 }, 544 { 545 name: "can remove self with flag", 546 config: org.Config{}, 547 opt: options{ 548 maximumDelta: 1, 549 requireSelf: false, 550 }, 551 admins: []string{"me"}, 552 remove: []string{"me"}, 553 }, 554 { 555 name: "reject same person with both roles", 556 config: org.Config{ 557 Admins: []string{"me"}, 558 Members: []string{"me"}, 559 }, 560 err: true, 561 }, 562 { 563 name: "github remove rpc fails", 564 admins: []string{"fail"}, 565 err: true, 566 }, 567 { 568 name: "github add rpc fails", 569 config: org.Config{ 570 Admins: []string{"fail"}, 571 }, 572 err: true, 573 }, 574 { 575 name: "require team member to be org member", 576 config: org.Config{ 577 Teams: map[string]org.Team{ 578 "group": { 579 Members: []string{"non-member"}, 580 }, 581 }, 582 }, 583 err: true, 584 }, 585 { 586 name: "require team maintainer to be org member", 587 config: org.Config{ 588 Teams: map[string]org.Team{ 589 "group": { 590 Maintainers: []string{"non-member"}, 591 }, 592 }, 593 }, 594 err: true, 595 }, 596 { 597 name: "require team members with upper name to be org member", 598 config: org.Config{ 599 Teams: map[string]org.Team{ 600 "foo": { 601 Members: []string{"Me"}, 602 }, 603 }, 604 Members: []string{"Me"}, 605 }, 606 members: []string{"Me"}, 607 }, 608 { 609 name: "require team maintainer with upper name to be org member", 610 config: org.Config{ 611 Teams: map[string]org.Team{ 612 "foo": { 613 Maintainers: []string{"Me"}, 614 }, 615 }, 616 Admins: []string{"Me"}, 617 }, 618 admins: []string{"Me"}, 619 }, 620 { 621 name: "disallow duplicate names", 622 config: org.Config{ 623 Teams: map[string]org.Team{ 624 "duplicate": {}, 625 "other": { 626 Previously: []string{"duplicate"}, 627 }, 628 }, 629 }, 630 err: true, 631 }, 632 { 633 name: "disallow duplicate names (single team)", 634 config: org.Config{ 635 Teams: map[string]org.Team{ 636 "foo": { 637 Previously: []string{"foo"}, 638 }, 639 }, 640 }, 641 err: true, 642 }, 643 { 644 name: "trivial case works", 645 }, 646 { 647 name: "some of everything", 648 config: org.Config{ 649 Admins: []string{"keep-admin", "new-admin"}, 650 Members: []string{"keep-member", "new-member"}, 651 }, 652 opt: options{ 653 maximumDelta: 0.5, 654 }, 655 admins: []string{"keep-admin", "drop-admin"}, 656 members: []string{"keep-member", "drop-member"}, 657 remove: []string{"drop-admin", "drop-member"}, 658 addMembers: []string{"new-member"}, 659 addAdmins: []string{"new-admin"}, 660 }, 661 { 662 name: "do not reinvite", 663 config: org.Config{ 664 Admins: []string{"invited-admin"}, 665 Members: []string{"invited-member"}, 666 }, 667 invitations: []string{"invited-admin", "invited-member"}, 668 }, 669 } 670 671 for _, tc := range cases { 672 t.Run(tc.name, func(t *testing.T) { 673 fc := &fakeClient{ 674 admins: sets.NewString(tc.admins...), 675 members: sets.NewString(tc.members...), 676 removed: sets.String{}, 677 newAdmins: sets.String{}, 678 newMembers: sets.String{}, 679 } 680 681 err := configureOrgMembers(tc.opt, fc, fakeOrg, tc.config, sets.NewString(tc.invitations...)) 682 switch { 683 case err != nil: 684 if !tc.err { 685 t.Errorf("Unexpected error: %v", err) 686 } 687 case tc.err: 688 t.Errorf("Failed to receive error") 689 default: 690 if err := cmpLists(tc.remove, fc.removed.List()); err != nil { 691 t.Errorf("Wrong users removed: %v", err) 692 } else if err := cmpLists(tc.addMembers, fc.newMembers.List()); err != nil { 693 t.Errorf("Wrong members added: %v", err) 694 } else if err := cmpLists(tc.addAdmins, fc.newAdmins.List()); err != nil { 695 t.Errorf("Wrong admins added: %v", err) 696 } 697 } 698 }) 699 } 700 } 701 702 type fakeTeamClient struct { 703 teams map[int]github.Team 704 max int 705 } 706 707 func makeFakeTeamClient(teams ...github.Team) *fakeTeamClient { 708 fc := fakeTeamClient{ 709 teams: map[int]github.Team{}, 710 } 711 for _, t := range teams { 712 fc.teams[t.ID] = t 713 if t.ID >= fc.max { 714 fc.max = t.ID + 1 715 } 716 } 717 return &fc 718 } 719 720 const fakeOrg = "random-org" 721 722 func (c *fakeTeamClient) CreateTeam(org string, team github.Team) (*github.Team, error) { 723 if org != fakeOrg { 724 return nil, fmt.Errorf("org must be %s, not %s", fakeOrg, org) 725 } 726 if team.Name == "fail" { 727 return nil, errors.New("injected CreateTeam error") 728 } 729 c.max++ 730 team.ID = c.max 731 c.teams[team.ID] = team 732 return &team, nil 733 734 } 735 736 func (c *fakeTeamClient) ListTeams(name string) ([]github.Team, error) { 737 if name == "fail" { 738 return nil, errors.New("injected ListTeams error") 739 } 740 var teams []github.Team 741 for _, t := range c.teams { 742 teams = append(teams, t) 743 } 744 return teams, nil 745 } 746 747 func (c *fakeTeamClient) DeleteTeam(id int) error { 748 switch _, ok := c.teams[id]; { 749 case !ok: 750 return fmt.Errorf("not found %d", id) 751 case id < 0: 752 return errors.New("injected DeleteTeam error") 753 } 754 delete(c.teams, id) 755 return nil 756 } 757 758 func (c *fakeTeamClient) EditTeam(team github.Team) (*github.Team, error) { 759 id := team.ID 760 t, ok := c.teams[id] 761 if !ok { 762 return nil, fmt.Errorf("team %d does not exist", id) 763 } 764 switch { 765 case team.Description == "fail": 766 return nil, errors.New("injected description failure") 767 case team.Name == "fail": 768 return nil, errors.New("injected name failure") 769 case team.Privacy == "fail": 770 return nil, errors.New("injected privacy failure") 771 } 772 if team.Description != "" { 773 t.Description = team.Description 774 } 775 if team.Name != "" { 776 t.Name = team.Name 777 } 778 if team.Privacy != "" { 779 t.Privacy = team.Privacy 780 } 781 if team.ParentTeamID != nil { 782 t.Parent = &github.Team{ 783 ID: *team.ParentTeamID, 784 } 785 } else { 786 t.Parent = nil 787 } 788 c.teams[id] = t 789 return &t, nil 790 } 791 792 func TestFindTeam(t *testing.T) { 793 cases := []struct { 794 name string 795 teams map[string]github.Team 796 current string 797 previous []string 798 expected int 799 }{ 800 { 801 name: "will find current team", 802 teams: map[string]github.Team{ 803 "hello": {ID: 17}, 804 }, 805 current: "hello", 806 expected: 17, 807 }, 808 { 809 name: "team does not exist returns nil", 810 teams: map[string]github.Team{ 811 "unrelated": {ID: 5}, 812 }, 813 current: "hypothetical", 814 }, 815 { 816 name: "will find previous name", 817 teams: map[string]github.Team{ 818 "deprecated name": {ID: 1}, 819 }, 820 current: "current name", 821 previous: []string{"archaic name", "deprecated name"}, 822 expected: 1, 823 }, 824 { 825 name: "prioritize current when previous also exists", 826 teams: map[string]github.Team{ 827 "deprecated": {ID: 1}, 828 "current": {ID: 2}, 829 }, 830 current: "current", 831 previous: []string{"deprecated"}, 832 expected: 2, 833 }, 834 } 835 836 for _, tc := range cases { 837 t.Run(tc.name, func(t *testing.T) { 838 actual := findTeam(tc.teams, tc.current, tc.previous...) 839 switch { 840 case actual == nil: 841 if tc.expected != 0 { 842 t.Errorf("failed to find team %d", tc.expected) 843 } 844 case tc.expected == 0: 845 t.Errorf("unexpected team returned: %v", *actual) 846 case actual.ID != tc.expected: 847 t.Errorf("team %v != expected ID %d", actual, tc.expected) 848 } 849 }) 850 } 851 } 852 853 func TestConfigureTeams(t *testing.T) { 854 desc := "so interesting" 855 priv := org.Secret 856 cases := []struct { 857 name string 858 err bool 859 orgNameOverride string 860 config org.Config 861 teams []github.Team 862 expected map[string]github.Team 863 deleted []int 864 delta float64 865 }{ 866 { 867 name: "do nothing without error", 868 }, 869 { 870 name: "reject duplicated team names (different teams)", 871 err: true, 872 config: org.Config{ 873 Teams: map[string]org.Team{ 874 "hello": {}, 875 "there": {Previously: []string{"hello"}}, 876 }, 877 }, 878 }, 879 { 880 name: "reject duplicated team names (single team)", 881 err: true, 882 config: org.Config{ 883 Teams: map[string]org.Team{ 884 "hello": {Previously: []string{"hello"}}, 885 }, 886 }, 887 }, 888 { 889 name: "fail to list teams", 890 orgNameOverride: "fail", 891 err: true, 892 }, 893 { 894 name: "fail to create team", 895 config: org.Config{ 896 Teams: map[string]org.Team{ 897 "fail": {}, 898 }, 899 }, 900 err: true, 901 }, 902 { 903 name: "fail to delete team", 904 teams: []github.Team{ 905 {Name: "fail", ID: -55}, 906 }, 907 err: true, 908 }, 909 { 910 name: "create missing team", 911 teams: []github.Team{ 912 {Name: "old", ID: 1}, 913 }, 914 config: org.Config{ 915 Teams: map[string]org.Team{ 916 "new": {}, 917 "old": {}, 918 }, 919 }, 920 expected: map[string]github.Team{ 921 "old": {Name: "old", ID: 1}, 922 "new": {Name: "new", ID: 3}, 923 }, 924 }, 925 { 926 name: "reuse existing teams", 927 teams: []github.Team{ 928 {Name: "current", ID: 1}, 929 {Name: "deprecated", ID: 5}, 930 }, 931 config: org.Config{ 932 Teams: map[string]org.Team{ 933 "current": {}, 934 "updated": {Previously: []string{"deprecated"}}, 935 }, 936 }, 937 expected: map[string]github.Team{ 938 "current": {Name: "current", ID: 1}, 939 "updated": {Name: "deprecated", ID: 5}, 940 }, 941 }, 942 { 943 name: "delete unused teams", 944 teams: []github.Team{ 945 { 946 Name: "unused", 947 ID: 1, 948 }, 949 { 950 Name: "used", 951 ID: 2, 952 }, 953 }, 954 config: org.Config{ 955 Teams: map[string]org.Team{ 956 "used": {}, 957 }, 958 }, 959 expected: map[string]github.Team{ 960 "used": {ID: 2, Name: "used"}, 961 }, 962 deleted: []int{1}, 963 }, 964 { 965 name: "create team with metadata", 966 config: org.Config{ 967 Teams: map[string]org.Team{ 968 "new": { 969 TeamMetadata: org.TeamMetadata{ 970 Description: &desc, 971 Privacy: &priv, 972 }, 973 }, 974 }, 975 }, 976 expected: map[string]github.Team{ 977 "new": {ID: 1, Name: "new", Description: desc, Privacy: string(priv)}, 978 }, 979 }, 980 { 981 name: "allow deleting many teams", 982 teams: []github.Team{ 983 { 984 Name: "unused", 985 ID: 1, 986 }, 987 { 988 Name: "used", 989 ID: 2, 990 }, 991 }, 992 config: org.Config{ 993 Teams: map[string]org.Team{ 994 "used": {}, 995 }, 996 }, 997 expected: map[string]github.Team{ 998 "used": {ID: 2, Name: "used"}, 999 }, 1000 delta: 0.6, 1001 }, 1002 { 1003 name: "refuse to delete too many teams", 1004 teams: []github.Team{ 1005 { 1006 Name: "unused", 1007 ID: 1, 1008 }, 1009 { 1010 Name: "used", 1011 ID: 2, 1012 }, 1013 }, 1014 config: org.Config{ 1015 Teams: map[string]org.Team{ 1016 "used": {}, 1017 }, 1018 }, 1019 err: true, 1020 delta: 0.1, 1021 }, 1022 } 1023 1024 for _, tc := range cases { 1025 t.Run(tc.name, func(t *testing.T) { 1026 fc := makeFakeTeamClient(tc.teams...) 1027 orgName := tc.orgNameOverride 1028 if orgName == "" { 1029 orgName = fakeOrg 1030 } 1031 if tc.expected == nil { 1032 tc.expected = map[string]github.Team{} 1033 } 1034 if tc.delta == 0 { 1035 tc.delta = 1 1036 } 1037 actual, err := configureTeams(fc, orgName, tc.config, tc.delta) 1038 switch { 1039 case err != nil: 1040 if !tc.err { 1041 t.Errorf("unexpected error: %v", err) 1042 } 1043 case tc.err: 1044 t.Errorf("failed to receive error") 1045 case !reflect.DeepEqual(actual, tc.expected): 1046 t.Errorf("%#v != actual %#v", tc.expected, actual) 1047 } 1048 for _, id := range tc.deleted { 1049 if team, ok := fc.teams[id]; ok { 1050 t.Errorf("%d still present: %#v", id, team) 1051 } 1052 } 1053 }) 1054 } 1055 } 1056 1057 func TestConfigureTeam(t *testing.T) { 1058 old := "old value" 1059 cur := "current value" 1060 fail := "fail" 1061 pfail := org.Privacy(fail) 1062 whatev := "whatever" 1063 secret := org.Secret 1064 parent := 2 1065 cases := []struct { 1066 name string 1067 err bool 1068 teamName string 1069 parent *int 1070 config org.Team 1071 github github.Team 1072 expected github.Team 1073 }{ 1074 { 1075 name: "patch team when name changes", 1076 teamName: cur, 1077 config: org.Team{ 1078 Previously: []string{old}, 1079 }, 1080 github: github.Team{ 1081 ID: 1, 1082 Name: old, 1083 }, 1084 expected: github.Team{ 1085 ID: 1, 1086 Name: cur, 1087 }, 1088 }, 1089 { 1090 name: "patch team when description changes", 1091 teamName: whatev, 1092 parent: nil, 1093 config: org.Team{ 1094 TeamMetadata: org.TeamMetadata{ 1095 Description: &cur, 1096 }, 1097 }, 1098 github: github.Team{ 1099 ID: 2, 1100 Name: whatev, 1101 Description: old, 1102 }, 1103 expected: github.Team{ 1104 ID: 2, 1105 Name: whatev, 1106 Description: cur, 1107 }, 1108 }, 1109 { 1110 name: "patch team when privacy changes", 1111 teamName: whatev, 1112 parent: nil, 1113 config: org.Team{ 1114 TeamMetadata: org.TeamMetadata{ 1115 Privacy: &secret, 1116 }, 1117 }, 1118 github: github.Team{ 1119 ID: 3, 1120 Name: whatev, 1121 Privacy: string(org.Closed), 1122 }, 1123 expected: github.Team{ 1124 ID: 3, 1125 Name: whatev, 1126 Privacy: string(secret), 1127 }, 1128 }, 1129 { 1130 name: "patch team when parent changes", 1131 teamName: whatev, 1132 parent: &parent, 1133 config: org.Team{}, 1134 github: github.Team{ 1135 ID: 3, 1136 Name: whatev, 1137 Parent: &github.Team{ 1138 ID: 4, 1139 }, 1140 }, 1141 expected: github.Team{ 1142 ID: 3, 1143 Name: whatev, 1144 Parent: &github.Team{ 1145 ID: 2, 1146 }, 1147 Privacy: string(org.Closed), 1148 }, 1149 }, 1150 { 1151 name: "patch team when parent removed", 1152 teamName: whatev, 1153 parent: nil, 1154 config: org.Team{}, 1155 github: github.Team{ 1156 ID: 3, 1157 Name: whatev, 1158 Parent: &github.Team{ 1159 ID: 2, 1160 }, 1161 }, 1162 expected: github.Team{ 1163 ID: 3, 1164 Name: whatev, 1165 Parent: nil, 1166 }, 1167 }, 1168 { 1169 name: "do not patch team when values are the same", 1170 teamName: fail, 1171 parent: &parent, 1172 config: org.Team{ 1173 TeamMetadata: org.TeamMetadata{ 1174 Description: &fail, 1175 Privacy: &pfail, 1176 }, 1177 }, 1178 github: github.Team{ 1179 ID: 4, 1180 Name: fail, 1181 Description: fail, 1182 Privacy: fail, 1183 Parent: &github.Team{ 1184 ID: 2, 1185 }, 1186 }, 1187 expected: github.Team{ 1188 ID: 4, 1189 Name: fail, 1190 Description: fail, 1191 Privacy: fail, 1192 Parent: &github.Team{ 1193 ID: 2, 1194 }, 1195 }, 1196 }, 1197 { 1198 name: "fail to patch team", 1199 teamName: "team", 1200 parent: nil, 1201 config: org.Team{ 1202 TeamMetadata: org.TeamMetadata{ 1203 Description: &fail, 1204 }, 1205 }, 1206 github: github.Team{ 1207 ID: 1, 1208 Name: "team", 1209 Description: whatev, 1210 }, 1211 err: true, 1212 }, 1213 } 1214 1215 for _, tc := range cases { 1216 t.Run(tc.name, func(t *testing.T) { 1217 fc := makeFakeTeamClient(tc.github) 1218 err := configureTeam(fc, fakeOrg, tc.teamName, tc.config, tc.github, tc.parent) 1219 switch { 1220 case err != nil: 1221 if !tc.err { 1222 t.Errorf("unexpected error: %v", err) 1223 } 1224 case tc.err: 1225 t.Errorf("failed to receive expected error") 1226 case !reflect.DeepEqual(fc.teams[tc.expected.ID], tc.expected): 1227 t.Errorf("actual %+v != expected %+v", fc.teams[tc.expected.ID], tc.expected) 1228 } 1229 }) 1230 } 1231 } 1232 1233 func TestConfigureTeamMembers(t *testing.T) { 1234 cases := []struct { 1235 name string 1236 err bool 1237 members sets.String 1238 maintainers sets.String 1239 remove sets.String 1240 addMembers sets.String 1241 addMaintainers sets.String 1242 invitees sets.String 1243 team org.Team 1244 id int 1245 }{ 1246 { 1247 name: "fail when listing fails", 1248 id: teamID ^ 0xff, 1249 err: true, 1250 }, 1251 { 1252 name: "fail when removal fails", 1253 members: sets.NewString("fail"), 1254 err: true, 1255 }, 1256 { 1257 name: "fail when add fails", 1258 team: org.Team{ 1259 Maintainers: []string{"fail"}, 1260 }, 1261 err: true, 1262 }, 1263 { 1264 name: "some of everything", 1265 team: org.Team{ 1266 Maintainers: []string{"keep-maintainer", "new-maintainer"}, 1267 Members: []string{"keep-member", "new-member"}, 1268 }, 1269 maintainers: sets.NewString("keep-maintainer", "drop-maintainer"), 1270 members: sets.NewString("keep-member", "drop-member"), 1271 remove: sets.NewString("drop-maintainer", "drop-member"), 1272 addMembers: sets.NewString("new-member"), 1273 addMaintainers: sets.NewString("new-maintainer"), 1274 }, 1275 { 1276 name: "do not reinvitee invitees", 1277 team: org.Team{ 1278 Maintainers: []string{"invited-maintainer", "newbie"}, 1279 Members: []string{"invited-member"}, 1280 }, 1281 invitees: sets.NewString("invited-maintainer", "invited-member"), 1282 addMaintainers: sets.NewString("newbie"), 1283 }, 1284 { 1285 name: "do not remove pending invitees", 1286 team: org.Team{ 1287 Maintainers: []string{"keep-maintainer"}, 1288 Members: []string{"invited-member"}, 1289 }, 1290 maintainers: sets.NewString("keep-maintainer"), 1291 invitees: sets.NewString("invited-member"), 1292 remove: sets.String{}, 1293 }, 1294 } 1295 1296 for _, tc := range cases { 1297 if tc.id == 0 { 1298 tc.id = teamID 1299 } 1300 t.Run(tc.name, func(t *testing.T) { 1301 fc := &fakeClient{ 1302 admins: sets.StringKeySet(tc.maintainers), 1303 members: sets.StringKeySet(tc.members), 1304 invitees: sets.StringKeySet(tc.invitees), 1305 removed: sets.String{}, 1306 newAdmins: sets.String{}, 1307 newMembers: sets.String{}, 1308 } 1309 err := configureTeamMembers(fc, tc.id, tc.team) 1310 switch { 1311 case err != nil: 1312 if !tc.err { 1313 t.Errorf("Unexpected error: %v", err) 1314 } 1315 case tc.err: 1316 t.Errorf("Failed to receive error") 1317 default: 1318 if err := cmpLists(tc.remove.List(), fc.removed.List()); err != nil { 1319 t.Errorf("Wrong users removed: %v", err) 1320 } else if err := cmpLists(tc.addMembers.List(), fc.newMembers.List()); err != nil { 1321 t.Errorf("Wrong members added: %v", err) 1322 } else if err := cmpLists(tc.addMaintainers.List(), fc.newAdmins.List()); err != nil { 1323 t.Errorf("Wrong admins added: %v", err) 1324 } 1325 } 1326 1327 }) 1328 } 1329 } 1330 1331 func cmpLists(a, b []string) error { 1332 if a == nil { 1333 a = []string{} 1334 } 1335 if b == nil { 1336 b = []string{} 1337 } 1338 sort.Strings(a) 1339 sort.Strings(b) 1340 if !reflect.DeepEqual(a, b) { 1341 return fmt.Errorf("%v != %v", a, b) 1342 } 1343 return nil 1344 } 1345 1346 type fakeOrgClient struct { 1347 current github.Organization 1348 changed bool 1349 } 1350 1351 func (o *fakeOrgClient) GetOrg(name string) (*github.Organization, error) { 1352 if name == "fail" { 1353 return nil, errors.New("injected GetOrg error") 1354 } 1355 return &o.current, nil 1356 } 1357 1358 func (o *fakeOrgClient) EditOrg(name string, org github.Organization) (*github.Organization, error) { 1359 if org.Description == "fail" { 1360 return nil, errors.New("injected EditOrg error") 1361 } 1362 o.current = org 1363 o.changed = true 1364 return &o.current, nil 1365 } 1366 1367 func TestUpdateBool(t *testing.T) { 1368 yes := true 1369 no := false 1370 cases := []struct { 1371 name string 1372 have *bool 1373 want *bool 1374 end bool 1375 ret *bool 1376 }{ 1377 { 1378 name: "panic on nil have", 1379 want: &no, 1380 }, 1381 { 1382 name: "never change on nil want", 1383 want: nil, 1384 have: &yes, 1385 end: yes, 1386 ret: &no, 1387 }, 1388 { 1389 name: "do not change if same", 1390 want: &yes, 1391 have: &yes, 1392 end: yes, 1393 ret: &no, 1394 }, 1395 { 1396 name: "change if different", 1397 want: &no, 1398 have: &yes, 1399 end: no, 1400 ret: &yes, 1401 }, 1402 } 1403 1404 for _, tc := range cases { 1405 t.Run(tc.name, func(t *testing.T) { 1406 defer func() { 1407 wantPanic := tc.ret == nil 1408 r := recover() 1409 gotPanic := r != nil 1410 switch { 1411 case gotPanic && !wantPanic: 1412 t.Errorf("unexpected panic: %v", r) 1413 case wantPanic && !gotPanic: 1414 t.Errorf("failed to receive panic") 1415 } 1416 }() 1417 if tc.have != nil { // prevent overwriting what tc.have points to for next test case 1418 have := *tc.have 1419 tc.have = &have 1420 } 1421 ret := updateBool(tc.have, tc.want) 1422 switch { 1423 case ret != *tc.ret: 1424 t.Errorf("return value %t != expected %t", ret, *tc.ret) 1425 case *tc.have != tc.end: 1426 t.Errorf("end value %t != expected %t", *tc.have, tc.end) 1427 } 1428 }) 1429 } 1430 } 1431 1432 func TestUpdateString(t *testing.T) { 1433 no := false 1434 yes := true 1435 hello := "hello" 1436 world := "world" 1437 empty := "" 1438 cases := []struct { 1439 name string 1440 have *string 1441 want *string 1442 expected string 1443 ret *bool 1444 }{ 1445 { 1446 name: "panic on nil have", 1447 want: &hello, 1448 }, 1449 { 1450 name: "never change on nil want", 1451 want: nil, 1452 have: &hello, 1453 expected: hello, 1454 ret: &no, 1455 }, 1456 { 1457 name: "do not change if same", 1458 want: &world, 1459 have: &world, 1460 expected: world, 1461 ret: &no, 1462 }, 1463 { 1464 name: "change if different", 1465 want: &empty, 1466 have: &hello, 1467 expected: empty, 1468 ret: &yes, 1469 }, 1470 } 1471 1472 for _, tc := range cases { 1473 t.Run(tc.name, func(t *testing.T) { 1474 defer func() { 1475 wantPanic := tc.ret == nil 1476 r := recover() 1477 gotPanic := r != nil 1478 switch { 1479 case gotPanic && !wantPanic: 1480 t.Errorf("unexpected panic: %v", r) 1481 case wantPanic && !gotPanic: 1482 t.Errorf("failed to receive panic") 1483 } 1484 }() 1485 if tc.have != nil { // prevent overwriting what tc.have points to for next test case 1486 have := *tc.have 1487 tc.have = &have 1488 } 1489 ret := updateString(tc.have, tc.want) 1490 switch { 1491 case ret != *tc.ret: 1492 t.Errorf("return value %t != expected %t", ret, *tc.ret) 1493 case *tc.have != tc.expected: 1494 t.Errorf("end value %s != expected %s", *tc.have, tc.expected) 1495 } 1496 }) 1497 } 1498 } 1499 1500 func TestConfigureOrgMeta(t *testing.T) { 1501 filled := github.Organization{ 1502 BillingEmail: "be", 1503 Company: "co", 1504 Email: "em", 1505 Location: "lo", 1506 Name: "na", 1507 Description: "de", 1508 HasOrganizationProjects: true, 1509 HasRepositoryProjects: true, 1510 DefaultRepositoryPermission: "not-a-real-value", 1511 MembersCanCreateRepositories: true, 1512 } 1513 yes := true 1514 no := false 1515 str := "random-letters" 1516 fail := "fail" 1517 read := org.Read 1518 1519 cases := []struct { 1520 name string 1521 orgName string 1522 want org.Metadata 1523 have github.Organization 1524 expected github.Organization 1525 err bool 1526 change bool 1527 }{ 1528 { 1529 name: "no want means no change", 1530 have: filled, 1531 expected: filled, 1532 change: false, 1533 }, 1534 { 1535 name: "fail if GetOrg fails", 1536 orgName: fail, 1537 err: true, 1538 }, 1539 { 1540 name: "fail if EditOrg fails", 1541 want: org.Metadata{Description: &fail}, 1542 err: true, 1543 }, 1544 { 1545 name: "billing diff causes change", 1546 want: org.Metadata{BillingEmail: &str}, 1547 expected: github.Organization{ 1548 BillingEmail: str, 1549 }, 1550 change: true, 1551 }, 1552 { 1553 name: "company diff causes change", 1554 want: org.Metadata{Company: &str}, 1555 expected: github.Organization{ 1556 Company: str, 1557 }, 1558 change: true, 1559 }, 1560 { 1561 name: "email diff causes change", 1562 want: org.Metadata{Email: &str}, 1563 expected: github.Organization{ 1564 Email: str, 1565 }, 1566 change: true, 1567 }, 1568 { 1569 name: "location diff causes change", 1570 want: org.Metadata{Location: &str}, 1571 expected: github.Organization{ 1572 Location: str, 1573 }, 1574 change: true, 1575 }, 1576 { 1577 name: "name diff causes change", 1578 want: org.Metadata{Name: &str}, 1579 expected: github.Organization{ 1580 Name: str, 1581 }, 1582 change: true, 1583 }, 1584 { 1585 name: "org projects diff causes change", 1586 want: org.Metadata{HasOrganizationProjects: &yes}, 1587 expected: github.Organization{ 1588 HasOrganizationProjects: yes, 1589 }, 1590 change: true, 1591 }, 1592 { 1593 name: "repo projects diff causes change", 1594 want: org.Metadata{HasRepositoryProjects: &yes}, 1595 expected: github.Organization{ 1596 HasRepositoryProjects: yes, 1597 }, 1598 change: true, 1599 }, 1600 { 1601 name: "default permission diff causes change", 1602 want: org.Metadata{DefaultRepositoryPermission: &read}, 1603 expected: github.Organization{ 1604 DefaultRepositoryPermission: string(read), 1605 }, 1606 change: true, 1607 }, 1608 { 1609 name: "members can create diff causes change", 1610 want: org.Metadata{MembersCanCreateRepositories: &yes}, 1611 expected: github.Organization{ 1612 MembersCanCreateRepositories: yes, 1613 }, 1614 change: true, 1615 }, 1616 { 1617 name: "change all values at once", 1618 have: filled, 1619 want: org.Metadata{ 1620 BillingEmail: &str, 1621 Company: &str, 1622 Email: &str, 1623 Location: &str, 1624 Name: &str, 1625 Description: &str, 1626 HasOrganizationProjects: &no, 1627 HasRepositoryProjects: &no, 1628 MembersCanCreateRepositories: &no, 1629 DefaultRepositoryPermission: &read, 1630 }, 1631 expected: github.Organization{ 1632 BillingEmail: str, 1633 Company: str, 1634 Email: str, 1635 Location: str, 1636 Name: str, 1637 Description: str, 1638 HasOrganizationProjects: no, 1639 HasRepositoryProjects: no, 1640 MembersCanCreateRepositories: no, 1641 DefaultRepositoryPermission: string(read), 1642 }, 1643 change: true, 1644 }, 1645 } 1646 1647 for _, tc := range cases { 1648 t.Run(tc.name, func(t *testing.T) { 1649 if tc.orgName == "" { 1650 tc.orgName = "whatever" 1651 } 1652 fc := fakeOrgClient{ 1653 current: tc.have, 1654 } 1655 err := configureOrgMeta(&fc, tc.orgName, tc.want) 1656 switch { 1657 case err != nil: 1658 if !tc.err { 1659 t.Errorf("unexpected error: %v", err) 1660 } 1661 case tc.err: 1662 t.Errorf("failed to receive error") 1663 case tc.change != fc.changed: 1664 t.Errorf("changed %t != expected %t", fc.changed, tc.change) 1665 case !reflect.DeepEqual(fc.current, tc.expected): 1666 t.Errorf("current %#v != expected %#v", fc.current, tc.expected) 1667 } 1668 }) 1669 } 1670 } 1671 1672 func TestDumpOrgConfig(t *testing.T) { 1673 empty := "" 1674 hello := "Hello" 1675 details := "wise and brilliant exemplary human specimens" 1676 yes := true 1677 no := false 1678 perm := org.Write 1679 pub := org.Privacy("") 1680 cases := []struct { 1681 name string 1682 orgOverride string 1683 meta github.Organization 1684 members []string 1685 admins []string 1686 teams []github.Team 1687 teamMembers map[int][]string 1688 maintainers map[int][]string 1689 expected org.Config 1690 err bool 1691 }{ 1692 { 1693 name: "fails if GetOrg fails", 1694 orgOverride: "fail", 1695 err: true, 1696 }, 1697 { 1698 name: "fails if ListOrgMembers fails", 1699 err: true, 1700 members: []string{"hello", "fail"}, 1701 }, 1702 { 1703 name: "fails if ListTeams fails", 1704 err: true, 1705 teams: []github.Team{ 1706 { 1707 Name: "fail", 1708 ID: 3, 1709 }, 1710 }, 1711 }, 1712 { 1713 name: "fails if ListTeamMembersFails", 1714 err: true, 1715 teams: []github.Team{ 1716 { 1717 Name: "fred", 1718 ID: -1, 1719 }, 1720 }, 1721 }, 1722 { 1723 name: "basically works", 1724 meta: github.Organization{ 1725 Name: hello, 1726 MembersCanCreateRepositories: yes, 1727 DefaultRepositoryPermission: string(perm), 1728 }, 1729 members: []string{"george", "jungle", "banana"}, 1730 admins: []string{"james", "giant", "peach"}, 1731 teams: []github.Team{ 1732 { 1733 ID: 5, 1734 Name: "friends", 1735 Description: details, 1736 }, 1737 { 1738 ID: 6, 1739 Name: "enemies", 1740 }, 1741 { 1742 ID: 7, 1743 Name: "archenemies", 1744 Parent: &github.Team{ 1745 ID: 6, 1746 Name: "enemies", 1747 }, 1748 }, 1749 }, 1750 teamMembers: map[int][]string{ 1751 5: {"george", "james"}, 1752 6: {"george"}, 1753 7: {}, 1754 }, 1755 maintainers: map[int][]string{ 1756 5: {}, 1757 6: {"giant", "jungle"}, 1758 7: {"banana"}, 1759 }, 1760 expected: org.Config{ 1761 Metadata: org.Metadata{ 1762 Name: &hello, 1763 BillingEmail: &empty, 1764 Company: &empty, 1765 Email: &empty, 1766 Description: &empty, 1767 Location: &empty, 1768 HasOrganizationProjects: &no, 1769 HasRepositoryProjects: &no, 1770 DefaultRepositoryPermission: &perm, 1771 MembersCanCreateRepositories: &yes, 1772 }, 1773 Teams: map[string]org.Team{ 1774 "friends": { 1775 TeamMetadata: org.TeamMetadata{ 1776 Description: &details, 1777 Privacy: &pub, 1778 }, 1779 Members: []string{"george", "james"}, 1780 Maintainers: []string{}, 1781 Children: map[string]org.Team{}, 1782 }, 1783 "enemies": { 1784 TeamMetadata: org.TeamMetadata{ 1785 Description: &empty, 1786 Privacy: &pub, 1787 }, 1788 Members: []string{"george"}, 1789 Maintainers: []string{"giant", "jungle"}, 1790 Children: map[string]org.Team{ 1791 "archenemies": { 1792 TeamMetadata: org.TeamMetadata{ 1793 Description: &empty, 1794 Privacy: &pub, 1795 }, 1796 Members: []string{}, 1797 Maintainers: []string{"banana"}, 1798 Children: map[string]org.Team{}, 1799 }, 1800 }, 1801 }, 1802 }, 1803 Members: []string{"george", "jungle", "banana"}, 1804 Admins: []string{"james", "giant", "peach"}, 1805 }, 1806 }, 1807 } 1808 1809 for _, tc := range cases { 1810 t.Run(tc.name, func(t *testing.T) { 1811 orgName := "random-org" 1812 if tc.orgOverride != "" { 1813 orgName = tc.orgOverride 1814 } 1815 fc := fakeDumpClient{ 1816 name: orgName, 1817 members: tc.members, 1818 admins: tc.admins, 1819 meta: tc.meta, 1820 teams: tc.teams, 1821 teamMembers: tc.teamMembers, 1822 maintainers: tc.maintainers, 1823 } 1824 actual, err := dumpOrgConfig(fc, orgName) 1825 switch { 1826 case err != nil: 1827 if !tc.err { 1828 t.Errorf("unexpected error: %v", err) 1829 } 1830 case tc.err: 1831 t.Errorf("failed to receive error") 1832 default: 1833 fixup(actual) 1834 fixup(&tc.expected) 1835 if !reflect.DeepEqual(actual, &tc.expected) { 1836 a, _ := yaml.Marshal(*actual) 1837 e, _ := yaml.Marshal(tc.expected) 1838 t.Errorf("actual:\n%s != expected:\n%s", string(a), string(e)) 1839 } 1840 1841 } 1842 }) 1843 } 1844 } 1845 1846 type fakeDumpClient struct { 1847 name string 1848 members []string 1849 admins []string 1850 meta github.Organization 1851 teams []github.Team 1852 teamMembers map[int][]string 1853 maintainers map[int][]string 1854 } 1855 1856 func (c fakeDumpClient) GetOrg(name string) (*github.Organization, error) { 1857 if name != c.name { 1858 return nil, errors.New("bad name") 1859 } 1860 if name == "fail" { 1861 return nil, errors.New("injected GetOrg error") 1862 } 1863 return &c.meta, nil 1864 } 1865 1866 func (c fakeDumpClient) makeMembers(people []string) ([]github.TeamMember, error) { 1867 var ret []github.TeamMember 1868 for _, p := range people { 1869 if p == "fail" { 1870 return nil, errors.New("injected makeMembers error") 1871 } 1872 ret = append(ret, github.TeamMember{Login: p}) 1873 } 1874 return ret, nil 1875 } 1876 1877 func (c fakeDumpClient) ListOrgMembers(name, role string) ([]github.TeamMember, error) { 1878 switch { 1879 case name != c.name: 1880 return nil, fmt.Errorf("bad org: %s", name) 1881 case role == github.RoleAdmin: 1882 return c.makeMembers(c.admins) 1883 case role == github.RoleMember: 1884 return c.makeMembers(c.members) 1885 } 1886 return nil, fmt.Errorf("bad role: %s", role) 1887 } 1888 1889 func (c fakeDumpClient) ListTeams(name string) ([]github.Team, error) { 1890 if name != c.name { 1891 return nil, fmt.Errorf("bad org: %s", name) 1892 } 1893 1894 for _, t := range c.teams { 1895 if t.Name == "fail" { 1896 return nil, errors.New("injected ListTeams error") 1897 } 1898 } 1899 return c.teams, nil 1900 } 1901 1902 func (c fakeDumpClient) ListTeamMembers(id int, role string) ([]github.TeamMember, error) { 1903 var mapping map[int][]string 1904 switch { 1905 case id < 0: 1906 return nil, errors.New("injected ListTeamMembers error") 1907 case role == github.RoleMaintainer: 1908 mapping = c.maintainers 1909 case role == github.RoleMember: 1910 mapping = c.teamMembers 1911 default: 1912 return nil, fmt.Errorf("bad role: %s", role) 1913 } 1914 people, ok := mapping[id] 1915 if !ok { 1916 return nil, fmt.Errorf("team does not exist: %d", id) 1917 } 1918 return c.makeMembers(people) 1919 } 1920 1921 func fixup(ret *org.Config) { 1922 if ret == nil { 1923 return 1924 } 1925 sort.Strings(ret.Members) 1926 sort.Strings(ret.Admins) 1927 for name, team := range ret.Teams { 1928 sort.Strings(team.Members) 1929 sort.Strings(team.Maintainers) 1930 sort.Strings(team.Previously) 1931 ret.Teams[name] = team 1932 } 1933 } 1934 1935 func TestOrgInvitations(t *testing.T) { 1936 cases := []struct { 1937 name string 1938 opt options 1939 invitees sets.String // overrides 1940 expected sets.String 1941 err bool 1942 }{ 1943 { 1944 name: "do not call on empty options", 1945 invitees: sets.NewString("him", "her", "them"), 1946 expected: sets.String{}, 1947 }, 1948 { 1949 name: "call if fixOrgMembers", 1950 opt: options{ 1951 fixOrgMembers: true, 1952 }, 1953 invitees: sets.NewString("him", "her", "them"), 1954 expected: sets.NewString("him", "her", "them"), 1955 }, 1956 { 1957 name: "call if fixTeamMembers", 1958 opt: options{ 1959 fixTeamMembers: true, 1960 }, 1961 invitees: sets.NewString("him", "her", "them"), 1962 expected: sets.NewString("him", "her", "them"), 1963 }, 1964 { 1965 name: "ensure case normalization", 1966 opt: options{ 1967 fixOrgMembers: true, 1968 fixTeamMembers: true, 1969 }, 1970 invitees: sets.NewString("MiXeD", "lower", "UPPER"), 1971 expected: sets.NewString("mixed", "lower", "upper"), 1972 }, 1973 { 1974 name: "error if list fails", 1975 opt: options{ 1976 fixTeamMembers: true, 1977 fixOrgMembers: true, 1978 }, 1979 invitees: sets.NewString("erick", "fail"), 1980 err: true, 1981 }, 1982 } 1983 1984 for _, tc := range cases { 1985 t.Run(tc.name, func(t *testing.T) { 1986 fc := &fakeClient{ 1987 invitees: tc.invitees, 1988 } 1989 actual, err := orgInvitations(tc.opt, fc, "random-org") 1990 switch { 1991 case err != nil: 1992 if !tc.err { 1993 t.Errorf("unexpected error: %v", err) 1994 } 1995 case tc.err: 1996 t.Errorf("failed to receive an error") 1997 case !reflect.DeepEqual(actual, tc.expected): 1998 t.Errorf("%#v != expected %#v", actual, tc.expected) 1999 } 2000 }) 2001 } 2002 }