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