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  }